codegen

package
v0.0.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 16, 2026 License: MIT Imports: 31 Imported by: 0

Documentation

Overview

Package codegen — config_k_gen.go is the MIGRATION half of the config-as-KCL story: it projects an existing per-env `config.<env>.yaml` into the user-owned KCL values file `deploy/kcl/<env>/config.k`.

Where config_schema_gen.go emits the TYPE (`schema AppConfig` + `schema ConfigSecretRef`) and config_projection_gen.go emits the BEHAVIOR (appConfigEnvVars / appConfigConfigMap), THIS file emits the VALUES — a single `app_config: AppConfig = { ... }` instance the two generated functions are applied to. Unlike the schema/projection files (generated, DO NOT EDIT), config.k is USER-OWNED after migration: it is the one-time projection of the sibling yaml into the typed model, and the author edits it thereafter.

The migration is faithful to how `config.<env>.yaml` is authored — SPARSE. Only keys the user actually set are materialized; every other field falls back to its AppConfig schema default (which the Go projector honors too: a non-sensitive field with no per-env value is skipped, and a sensitive field with no override gets the default `<project>-secrets` backend).

Field projection rules (mirroring deploy_config_gen.go so the resulting AppConfig renders byte-identical env output via the projection functions):

  • non-sensitive key with a value -> `<field> = <typed literal>` (str/duration quoted, int/float bare, bool True/False).
  • sensitive key whose value is a "${NAME}" / "${NAME#KEY}" secret reference -> `<field> = ConfigSecretRef { name = "<NAME>", key = "<KEY or lower(env_var)>" }`, parsed EXACTLY like deploy_config_gen.go:parseSecretRef.
  • sensitive key absent (or present but not a parseable ${...} ref) -> omitted; the schema's default-backend ConfigSecretRef applies, which is exactly what the Go projector wires for an un-overridden sensitive field.

Phase 3 is ADDITIVE: this emitter is standalone + tested and is NOT wired into the generate pipeline. It does not retire `config.<env>.yaml` and does not touch the Go projector (deploy_config_gen.go). The Phase 4 cutover owns swapping the projector for the typed path once parity is proven.

Package codegen — config_native_emit.go wires the three KCL-native config emitters (config_schema_gen.go, config_projection_gen.go, config_k_gen.go) into the generate pipeline. It is the Phase-4 counterpart to deploy_config_gen.go's GenerateDeployConfig (the Go projector that renders the per-env config_gen.k).

Two ownership tiers, mirroring the design:

  • config_schema.k + config_projection.k are PROJECT-LEVEL and Tier-1 forge-owned (writeForgeOwned): one pair per project, regenerated from proto on every `forge generate`. They carry the config TYPE and the projection BEHAVIOR — the role config_gen.k's schema/env-list logic played, now factored out of the per-env files.
  • deploy/kcl/<env>/config.k is PER-ENV and USER-OWNED (write-if-absent): the one-time migration of config.<env>.yaml into a typed AppConfig instance. forge scaffolds it once and never clobbers later edits.

Package codegen — config_projection_gen.go emits the VALUE-projection half that pairs with the TYPE half in config_schema_gen.go.

Where config_schema_gen.go projects the SHAPE (`schema AppConfig`), this emitter projects the BEHAVIOR: one generated KCL function that turns a typed `AppConfig` value into the agnostic-core env model an agnostic `forge.Service` consumes —

  • appConfigEnvMap(c) -> {str: forge.EnvSource} the agnostic-core env MAP (kcl/core.k), keyed by ENV_VAR name, one entry per config field with a non-empty env_var:
  • non-sensitive -> {value = <the value read off c>} (lowered INLINE — the typed field converted to a string, no ConfigMap object or reference)
  • sensitive -> {from_secret = {name = c.<field>.name, key = c.<field>.key}} (reads the typed ConfigSecretRef off the value)

This REPLACES the old `appConfigEnvVars(...) -> [forge.EnvVar]` list projection. The env MAP is the idiomatic authoring shape: a service consumes config the native way —

env = appConfigEnvMap(app_config) | { <service extras> }

— composing config-first with native KCL map-merge `|` (last-wins), so a service extra of the same env-var NAME overrides the config entry, and duplicate keys are structurally impossible (no `env_merge`). The k8s adapter then projects the map to `[EnvVar]` via `env_project` (core.k).

Non-sensitive config values are lowered INLINE as `{value = ...}` rather than through a ConfigMap object + reference — simpler is better: it drops the whole ConfigMap object and its cross-reference wiring. Only sensitive fields still route out-of-band, through `from_secret`.

The per-field metadata (env-var name, sensitive flag, value expression) is BAKED into the generated code from the proto — the function is generated, not generic — so the emitted KCL is a straight-line map/dict literal with no runtime reflection.

This is ADDITIVE: a standalone emitter + test, NOT wired into the generate pipeline, and it does NOT touch deploy_config_gen.go (the current Go projector). The runtime Go loader (pkg/config/loader.go, which binds env by each field's env_var) is unchanged.

The sensitive branch reads the typed ConfigSecretRef off the AppConfig value (from_secret.name = c.<field>.name, key = c.<field>.key). The default backend (name = "<project>-secrets", key = lower(env_var)) is supplied by the AppConfig field's SCHEMA DEFAULT (see config_schema_gen.go), and a per-env override flows through as a ConfigSecretRef the migration (config_k_gen.go) writes into config.k — so an author who overrides the backing Secret/key gets that here unchanged.

Package codegen — config_schema_gen.go emits a typed KCL `schema AppConfig` projected from the proto config fields (the `(forge.v1.config)`-annotated fields of proto/config/v1/config.proto).

This is the TYPE half of the config-as-KCL story. Where deploy_config_gen.go projects per-env VALUE literals ([forge.EnvVar] + [forge.ConfigMap]) from the sibling `config.<env>.yaml`, this emitter projects the SHAPE: one KCL schema whose fields mirror the proto config fields, so an env authors its config as a typed `AppConfig { ... }` instance in `deploy/kcl/<env>/config.k` and KCL type-checks it against the proto's own contract (required fields, types, defaults) at load time.

Phase 1 (this file) is ADDITIVE: it is a standalone emitter + test and is NOT yet wired into the generate pipeline. The runtime Go loader (pkg/config/loader.go), which binds env by each field's `env_var` annotation via descriptor reflection, is unchanged — the KCL schema describes the same fields, so the two stay coherent by construction.

Package codegen renders Go source files for the canonical scaffolds forge produces: handler stubs, CRUD handlers, authorizer, auth/tenant middleware, bootstrap wiring, mock services, and config loaders.

The behavioural surface is split into three small Services so callers can mock just the concern they touch:

  • Service — the file-emission orchestrator (every Generate*).
  • Parser — descriptor / proto / go.mod parsing (no I/O writes).
  • Inspector — Go AST source inspection (fallible-constructor and Deps-DB-field detection on user packages).

Data carriers (ServiceDef, EntityDef, FieldKind, ConfigField, the *TemplateData and *MethodData structs, MissingHandlerResult) remain as plain types — they have no behavioural seam to mock.

Package codegen — deploy_config_gen.go renders per-environment `deploy/kcl/<env>/config_gen.k` files, projecting the project's per-env config map (from forge.yaml + sibling files) plus the proto-level ConfigFieldOptions annotations into KCL EnvVar lists AND a generated ConfigMap resource.

The generated file declares one CATEGORY_ENV list per category present in the proto, plus an APP_ENV list for fields without a category, plus a CONFIG_MAPS list holding the project-owned ConfigMap (one per env, `<project>-<env>-config`) populated with all non-sensitive values. The hand-edited `main.k` for the env imports `deploy.kcl.<env>.config_gen`, concatenates the EnvVar lists into the application's `env_vars`, and assigns CONFIG_MAPS to `Environment.config_maps` so the render layer emits a `kind: ConfigMap` resource alongside the Deployments.

Generation rules:

  • For non-sensitive fields where the env config provides a value: the value is added to the generated ConfigMap's `data` map AND the EnvVar is emitted as `config_map_ref = "<project>-<env>-config", config_map_key = ENV_VAR`. The rendered Deployment env entry uses `valueFrom.configMapKeyRef`, so a `kubectl edit configmap` change propagates to pods on the next restart without rebuilding the Deployment manifest.
  • For non-sensitive fields with no value: skip (the binary's proto-derived default applies at startup).
  • For sensitive fields: emit `EnvVar { name = ENV_VAR, secret_ref = "<project>-secrets", secret_key = "<env_var lowercased>" }`. If the env config's value is a "${SECRET_NAME}" reference, the secret_ref is taken from the reference body instead. To override the secret key as well, use "${SECRET_NAME#secret-key}" (e.g. "${db-credentials#database-url}") — handy for projects whose existing cluster secrets use kebab-case keys that don't match the forge default of lowercase env_var. The Secret resource itself is NOT generated; it's expected to be provisioned out-of-band (sealed-secrets, ESO, manual `kubectl create secret`, etc.).

This replaces the hand-curated DB_ENV / NATS_ENV / STRIPE_ENV groups that projects accumulate in deploy/kcl/base.k as soon as they grow more than a couple of secret-backed knobs.

Package codegen — geninput.go defines GenContext, the shared project-scoped context that every emitter needs (where the project lives, its module path, and the checksum tracker that keeps Tier-1 output recorded in .forge/hashes.json).

Historically the core emitters threaded ProjectDir / ModulePath / *checksums.FileChecksums through 7-12 positional parameters each. Newer emitters (mcp_gen, ingress_k3d_gen, deploy_config_gen) adopted a per-emitter GenInput struct instead. GenContext finishes that migration: GenInput structs EMBED it so the three fields are declared once and accessed uniformly (in.ProjectDir, in.ModulePath, in.Checksums) via Go's field promotion, while the per-emitter struct adds only the fields that emitter actually varies on.

why embed rather than a *GenContext field: promotion keeps every existing `in.ProjectDir` / `in.Checksums` reference inside an emitter compiling unchanged, so converting an emitter to the struct form is a signature change at the call site only — the body is untouched.

Package codegen — ingress_k3d_gen.go writes deploy/k3d-ports.yaml, a small YAML fragment carrying the host→cluster port mappings the k3d loadbalancer needs to expose each Gateway listener.

The fragment is derived from the dev environment's KCL ingress topology, NOT hand-written. `forge cluster up` merges this fragment into the user-owned deploy/k3d.yaml at create time so k3d's ports block reflects the current gateway listeners. Keeping the merge at cluster-up time (rather than baking ports into k3d.yaml directly) lets the user edit `deploy/k3d.yaml` freely without forge clobbering it, while still keeping the listener ports in lockstep with whatever the project's Gateway resources declare.

Shape (one entry per listener across all dev-env gateways):

# Generated by `forge generate`. Do not edit — regenerated from
# deploy/kcl/dev/ingress.k.
ports:
  - port: 18080:18080
    nodeFilters: [loadbalancer]
  - port: 19190:19190
    nodeFilters: [loadbalancer]

Empty gateway list → no file written. Stale file from a previous generate (e.g. ingress feature was just disabled) is cleaned up by the caller, not here.

Package codegen — mcp_gen.go writes gen/mcp/manifest.json, a JSON manifest mapping every RPC in the project to an MCP tool schema.

The manifest exists so agent hosts (and any other MCP-aware tool) can discover the project's Connect RPCs as callable tools without bespoke per-project wiring. It is the static-descriptor sibling of `gen/forge_descriptor.json` — same source of truth (the parsed proto tree), different consumer (MCP servers vs forge's own codegen).

Shape (per RPC, one entry per service.method). Field names match the MCP specification's tool descriptor so the `tools` array can be returned verbatim as the result of `tools/list` from an MCP server. Extra forge-specific metadata (service / method / procedure / auth_required / idempotency_key / streaming) sits at the top level because MCP clients ignore unknown fields — the same JSON is both a valid MCP tool entry AND a forge-aware tool description.

{
  "_generated": "forge",
  "schema_version": "1.1",
  "project": "<project name>",
  "tools": [
    {
      "name": "<service_snake>__<rpc_snake>",
      "description": "<RPC doc-comment, or empty>",
      "inputSchema":  {"type": "object", "properties": {...}, "required": [...], "$defs": {...}},
      "outputSchema": {"type": "object", "properties": {...}, "$defs": {...}},
      "service": "<ServiceName>",
      "method": "<RpcName>",
      "procedure": "/<package>.<ServiceName>/<RpcName>",
      "auth_required": true|false,
      "idempotency_key": false,
      "streaming": "server|client|bidi"  // optional, only on streaming RPCs
    }
  ]
}

Schema depth (schema_version 1.1): each inputSchema/outputSchema is a SELF-CONTAINED JSON Schema. Nested proto messages are emitted in full via a top-level "$defs" block keyed by fully-qualified message/enum name, with "$ref": "#/$defs/<fq-name>" at every field site. One definition per message regardless of how many fields use it, which makes depth unlimited without size blowup and makes recursion terminate (self-referential messages reference themselves via $ref instead of inlining forever). $defs is per-schema rather than manifest-global because MCP clients receive each tool's inputSchema as a standalone document — a cross-document $ref would be unresolvable on the client side. It also lets input and output projections of the same message differ (required lists exist only in input schemas; see schemaForType).

Fallback: descriptors produced by forge versions without the deep type graph (ServiceDef.Schemas absent) degrade to the historic one-level-deep projection so the manifest never regresses below what it used to publish.

Empty service list → no manifest file written. An MCP host querying a service-less project should treat the absent file as "no tools"; emitting a tools:[] would imply the project deliberately publishes zero tools, which is a stronger statement than "this project hasn't scaffolded any services yet".

Code generated by forge. DO NOT EDIT. forge:hash=2ff67de49e154d911fd47538e48398f93f4d70ee9401202d884f6dd7b0c799bb forge-owned: regenerated every run — do not edit (forge disown to take ownership) Source: contract.go in this package.

To customize: edit contract.go (the interface IS the public surface) and re-run "forge generate". This file is regenerated unconditionally.

Index

Constants

View Source
const ComponentsJSONRelPath = "deploy/kcl/components_gen.json"

ComponentsJSONRelPath is the project-relative path of the generated denormalized component data. The per-env `deploy/kcl/<env>/main.k` loads it (via forge.components.load_components) and lets the forge KCL Component schema hierarchy expand each entry into k8s resources. It is a lockfile-class projection of forge.yaml — regenerated every run, untracked, owned 100% by forge (see GenerateComponentsJSON).

View Source
const ConfigSchemaModule = "config_schema"

ConfigSchemaModule is the KCL module name (and filename stem) the config schema is emitted under: deploy/kcl/config_schema.k. Both the projection file (config_projection.k) and each per-env values file (config.k) import it by THIS name and qualify the AppConfig / ConfigSecretRef schemas as `config_schema.AppConfig` etc. — KCL does not share top-level symbols across separately-imported modules, so the reference must be qualified.

View Source
const ServiceRowPrefix = "serviceRow"

ServiceRowPrefix is the name prefix of the generated per-service row constructors in pkg/app/services_gen.go ("serviceRow" + FieldName, e.g. serviceRowBilling). The cli layer's registration parser matches identifiers in the user-owned pkg/app/services.go by this prefix, so the template (services_gen.go.tmpl / services.go.tmpl) and the parser must agree on it.

Variables

This section is empty.

Functions

func AssignBootstrapAliases

func AssignBootstrapAliases(services []BootstrapServiceData, packages []BootstrapPackageData, workers []BootstrapWorkerData, operators []BootstrapOperatorData)

AssignBootstrapAliases populates the Alias field on every BootstrapComponentData across services, packages, workers, and operators. When the .Package fields are unique across all four roles, each Alias equals its Package (default Go import alias — preserves the original codegen output). When two roles share a .Package value (e.g. service "billing" + internal package "billing"), the conflicting component(s) get a role-prefixed alias ("svcBilling", "pkgBilling", "wkrBilling", "opBilling") so the import line in bootstrap.go can alias the import and every reference site can use the alias unambiguously.

This is purely additive — when there's no collision the default alias matches Package and the rendered bootstrap is identical to pre-aliasing output.

Internally delegates to ResolveCollisionNaming so wire_gen and bootstrap derive their function/field names from the same rule.

func AttachEntityMeta

func AttachEntityMeta(page *PageTemplateData, entity EntityDef)

AttachEntityMeta enriches a PageTemplateData with typed field metadata from the matched proto entity definition. The page generator calls this after pairing a CRUD RPC group with its EntityDef — the same pairing that gates page emission — so templates can emit fully typed columns, search fields, and detail rows.

func CollisionCounts

func CollisionCounts(services []BootstrapServiceData, packages []BootstrapPackageData, workers []BootstrapWorkerData, operators []BootstrapOperatorData) map[string]int

CollisionCounts returns a map of Go-package-name → occurrence count across services, packages, workers, and operators. A count > 1 means a cross-role collision (e.g. service "billing" + internal package "billing") and is the trigger for role-prefixed aliasing in AssignBootstrapAliases. Exposed so wire_gen and other generators can derive the SAME collision-aware FieldName that bootstrap uses without duplicating the bookkeeping.

func ComponentsToJSON

func ComponentsToJSON(projectName string, components []config.ComponentConfig) ([]byte, error)

ComponentsToJSON projects the forge.yaml component list to the denormalized JSON document. Deterministic: ports are sorted by name and components keep forge.yaml order so re-generation is idempotent.

func ComputeTestHelperName

func ComputeTestHelperName(servicePkg, projectDir string) string

ComputeTestHelperName returns the suffix used by the `app.NewTest<X>` and `app.NewTest<X>Server` factories generated into pkg/app/testing.go. When the service's Go-package name collides with an internal package directory of the same name (e.g. service `billing` + `internal/billing/`), the bootstrap testing generator disambiguates by prefixing "Svc" (NewTestSvcBilling). This helper mirrors that rule so test scaffolds emit the same identifier the factory actually has.

projectDir may be empty (no project context); in that case there's no collision detection possible and the result is the no-collision form. The collision rule matches GenerateBootstrapTesting's pkgCount logic.

func ConfigFieldNamesFromMessages

func ConfigFieldNamesFromMessages(messages []ConfigMessage) map[string]bool

ConfigFieldNamesFromMessages returns a map of Go field names present in the given config messages. Used by templates to conditionally include code blocks that reference specific config fields.

func ConvNeedsTimestamppb

func ConvNeedsTimestamppb(convs []EntityConvTemplateData) bool

ConvNeedsTimestamppb reports whether any assignment uses timestamppb.

func DefaultConfigFieldNames

func DefaultConfigFieldNames() map[string]bool

DefaultConfigFieldNames returns the field names from the default scaffold config proto. Used at initial project scaffold time before the config proto has been parsed by the generator.

func DetectConstructorType

func DetectConstructorType(dir string) (string, error)

DetectConstructorType returns the pretty-printed FIRST return type of the exported `func New(...)` in dir — the type the constructor produces. For a handler service whose New returns (*Service, error) it is "*Service"; for an internal package whose New returns Service (the contract interface) it is "Service". The qualifier prefix (the package selector) is added by the caller from the component's import alias.

Returns "" when the directory has no parseable New or the result list is empty — the caller falls back to the bootstrap default (*Service).

This is what makes the generated Services registry field type AND the inject_gen local-var assignment match the constructor exactly, regardless of whether the component exposes a concrete *Service struct (handlers) or a Service interface (internal packages) — closing the `*item.Service` vs `item.Service` assignability mismatch.

func DetectDepsDBField

func DetectDepsDBField(dir string) (bool, error)

DetectDepsDBField checks whether the Deps struct in the given directory has a field of type orm.Context (indicating the service needs a database). It parses all non-test .go files and looks for a type Deps struct with a field whose type is orm.Context. Returns true if the Deps struct has a DB field, false otherwise.

func DetectFallibleConstructor

func DetectFallibleConstructor(dir string) (bool, error)

DetectFallibleConstructor checks whether the exported New function in the given directory returns an error as its last result (i.e. returns (T, error)). It parses all non-test .go files and looks for a top-level func New(...). Returns true if the constructor is fallible, false otherwise. If the directory doesn't exist or contains no New function, returns false.

func EntityDefToPlanEntity

func EntityDefToPlanEntity(entity EntityDef) config.PlanEntity

EntityDefToPlanEntity converts an EntityDef to the PlanEntity shape the ORM generator consumes.

The field source is the entity's COLUMNS — the introspected applied schema — never the wire message. A column added by a hand-written migration appears here (and on the generated struct) without any proto involvement; a wire-only field never reaches the database layer. SQL is the schema truth.

func EntityDefsToPlanEntities

func EntityDefsToPlanEntities(entities []EntityDef) []config.PlanEntity

EntityDefsToPlanEntities converts a slice of EntityDef to a slice of PlanEntity.

func ExtractPlaceholderType

func ExtractPlaceholderType(text string) string

ExtractPlaceholderType returns the target type from a `forge:placeholder: <Type>` comment marker, or "" when no marker is present.

Recognition is line-based after stripping leading `//`, leading/ trailing whitespace; the marker must be the whole line:

// forge:placeholder: user.Repository
UserRepo any

Or the inline-comment slot:

UserRepo any // forge:placeholder: user.Repository

Both forms are accepted — same convention as `// forge:optional-dep`. The colon is required (it carries data, unlike optional-dep which is a bare directive). Whitespace after the colon is tolerated.

Exported so the wire-coverage lint can share the exact same recognition logic — the rule is "if the field has a placeholder AND the field's declared type is `any`, the placeholder is unresolved and lint must error."

func ExtractPlaceholderTypeCommentGroup

func ExtractPlaceholderTypeCommentGroup(cg *ast.CommentGroup) string

ExtractPlaceholderTypeCommentGroup is the AST-level analog of ExtractPlaceholderType. Same parity reason as HasOptionalDepMarkerCommentGroup: Go's CommentGroup.Text() drops the no-space directive form, so a comment written as `//forge:placeholder: user.Repository` (without the space) would be silently invisible to the .Text()-based scan. Iterating cg.List and trimming the raw markers preserves both shapes.

Returns the placeholder target type, or "" when no marker is present.

func GenerateAuthMiddleware

func GenerateAuthMiddleware(cfg *config.AuthConfig, modulePath string, skipMethods []string, targetDir string, cs *checksums.FileChecksums) error

GenerateAuthMiddleware renders the auth middleware templates and writes the generated files into pkg/middleware/ of the target project. It generates:

  • pkg/middleware/auth_gen.go (always regenerated — DO NOT EDIT)
  • pkg/middleware/auth_validator.go (only if API key auth is configured and file doesn't exist)

cs is the project's checksum tracker; passing it ensures the generated files do not show up as orphans in `forge audit`. A nil cs is tolerated (the file is still written) so callers without an active generate cycle can use the helper too.

func GenerateAuthorizer

func GenerateAuthorizer(services []ServiceDef, modulePath string, targetDir string, skipDirs map[string]bool, cs *checksums.FileChecksums) error

GenerateAuthorizer generates authorizer_gen.go for each service whose handler directory exists. The generated file contains a methodRoles map and a role-checking CanAccess/Can implementation. It is always generated (even with zero annotated methods) so that the companion authorizer.go can unconditionally reference GeneratedAuthorizer without compilation errors.

cs is the project's checksum tracker — passing it ensures every emitted authorizer_gen.go is recorded so `forge audit` doesn't flag it as an orphan. A nil cs is tolerated.

skipDirs lists handlers/<dir> leaves the directory sweep below must NOT touch — the dirs of tombstoned (types-only) services: services that pkg/app/services.go deliberately does not register (the row was deleted, a comment names the serving binary). Their services never appear in the (already row-filtered) services slice, so without the skip the sweep would misread a retired handler dir as an orphaned scaffold and re-emit authorizer_gen.go into it, re-adding the path to WrittenThisRun and hiding it from the stale-cleanup sweep. Keys are the snake package form (naming.ServicePackage); nil means no types-only services.

func GenerateBootstrapTesting

func GenerateBootstrapTesting(in BootstrapTestingGenInput) error

func GenerateCRUDHandlers

func GenerateCRUDHandlers(svc ServiceDef, crudMethods []CRUDMethod, modulePath string, projectDir string, cs *checksums.FileChecksums) error

GenerateCRUDHandlers generates handlers_crud_gen.go for a service with CRUD methods. It skips methods that already exist in user-owned handler files.

cs is the project's checksum tracker. Passing it ensures the rendered handlers_crud_gen.go is recorded so it doesn't show up as an orphan in `forge audit`. A nil cs is tolerated.

func GenerateCRUDTests

func GenerateCRUDTests(svc ServiceDef, crudMethods []CRUDMethod, modulePath string, projectDir string, cs *checksums.FileChecksums) error

GenerateCRUDTests generates handlers_crud_gen_test.go (unit-test frames, no build tag — runs in the default `go test ./...`) and handlers_crud_integration_test.go (lifecycle / tenant / pagination / filter / NotFound suites guarded by `//go:build integration`) for a service with CRUD methods.

cs is the project's checksum tracker. Both scaffold files are recorded when actually written; once the user clears every FORGE_SCAFFOLD marker the file becomes user-owned and forge stops re-rendering it (and stops updating the checksum). A nil cs is tolerated.

func GenerateCmdCommands

func GenerateCmdCommands(targetDir, bin string) error

GenerateCmdCommands scaffolds cmd/<bin>/cmd/commands.go — the user-owned cobra extension point newRootCmd consumes (userCommands(deps)). Written ONCE; never overwritten (Tier-2: the user owns the file the moment it exists). Second binaries register here as code with opt-in serverkit pieces instead of a parallel hand-rolled main().

bin is the primary binary name (the cmd/<bin>/cmd directory leaf). The template references it for accurate doc paths and the {{.Name}} display.

func GenerateCmdGroups

func GenerateCmdGroups(in CmdServiceGroupInput, targetDir string, cs *checksums.FileChecksums) error

GenerateCmdGroups renders the dir-nested command groups under cmd/<bin>/cmd (devspace idiom): ONE FILE PER SERVICE in the services/ SUBPACKAGE, each `New<X>Cmd(cmd.Deps)` whose RunE calls cmd.Serve() with a TYPED (*app.Components).Mount<Svc> selection. Each of the services/, workers/, and operators/ groups gets a register_gen.go anchor so the package compiles with zero items; the services anchor additionally carries the built-in collision NOTEs. Selection is compile-time typed — no string positional arg, no string→inventory lookup.

It ALSO scaffolds cmd/<bin>/main.go — the composition root — ONCE (write-if-absent) from the service rows. main.go is OWNED code thereafter: `forge add worker/operator` appends the constructor ref by hand. The per-worker/operator subcommand files are NOT emitted here — they are scaffold-once OWNED code (ScaffoldWorkerCmd / ScaffoldOperatorCmd).

cs is the project's checksum tracker — passing it keeps the (proto-derived) service files out of `forge audit`'s orphan list. A nil cs is tolerated.

func GenerateCmdMainRoot

func GenerateCmdMainRoot(targetDir, bin string, cs *checksums.FileChecksums) error

GenerateCmdMainRoot renders ONLY cmd/<bin>/main.go — the composition root — as a bare cmd.Execute() with no component constructors. The generate pipeline emits main.go via GenerateCmdGroups (which knows the full inventory); this exported entry lets the scaffold drop a composition root for a service project that has no proto pipeline (features.codegen=false), preserving the pre-refactor contract that main.go is always scaffolded for service kind.

func GenerateCmdServer

func GenerateCmdServer(messages []ConfigMessage, targetDir string, cs *checksums.FileChecksums) error

GenerateCmdServer re-renders cmd/server.go with config field awareness. Called during `forge generate` so that cmd/server.go stays in sync with the actual config proto fields.

This variant has no project-config access, so it renders WITHOUT the generated-auth call site (AuthProvider empty). The generate pipeline uses GenerateCmdServerWithFields, which threads the provider through.

cs is the project's checksum tracker — passing it keeps cmd/server.go out of `forge audit`'s orphan/user-edited lists. A nil cs is tolerated.

func GenerateCmdServerWithFields

func GenerateCmdServerWithFields(configFields map[string]bool, authProvider string, targetDir string, cs *checksums.FileChecksums) error

GenerateCmdServerWithFields renders cmd/server.go using a pre-built config field map (e.g. with migration fields stripped when the migrations feature is disabled) and the project's forge.yaml auth.provider (any spelling; normalized here).

func GenerateComponentsJSON

func GenerateComponentsJSON(projectDir, projectName string, components []config.ComponentConfig, cs *checksums.FileChecksums) error

GenerateComponentsJSON writes deploy/kcl/components_gen.json from the project's component list.

components_gen.json is a LOCKFILE-CLASS artifact: a pure, deterministic projection of forge.yaml's `components:` with ZERO user-editable surface. forge owns 100% of it and rewrites it byte-for-byte every run. It is therefore NOT registered in `.forge/hashes.json` and NOT subject to the Tier-1 stomp guard — detecting a "hand-edit" to a derived projection is meaningless (the next run discards it anyway), and TRACKING it would reintroduce the WIP-lane bookkeeping hazard TestE2ESelfCertParallelLaneSubsetCommit guards against: a committed `.forge/hashes.json` recording a render that was never committed makes a clean clone of HEAD refuse to regenerate. An always-regenerated, untracked file sidesteps that entirely (same posture gen/mcp/ manifest.json gets when its inputs are absent).

A stale entry under the legacy tracked path is cleared so an upgrade from a tracked-components_gen.json build can't leave a poison hash behind.

func GenerateCompose

func GenerateCompose(in InjectGenInput) error

GenerateCompose emits internal/app/compose.go: the EXPLICIT per-binary component construction site (the Components typed bag + NewComponents) that REPLACES the retired generated injector (inject_gen.go + app_services_gen.go). It constructs every registered component in TYPE-topological order and fills each Deps field BY TYPE — from another constructed component, from a field on the owned *Infra struct (providers.go), or from the conventional Logger/Config sources.

Returns an error listing every MissingProvider when a required collaborator field resolves to no producer and the matcher PROVES the Infra struct has no assignable field.

This is the live composition path: cmd-server composes OpenInfra → NewComponents → mount via the typed Mount<Svc> methods + AllWorkers / AllOperators. There is no by-type injector and no *Services god-struct.

func GenerateConfigKFromYAML

func GenerateConfigKFromYAML(fields []ConfigField, envConfig map[string]any, projectName string) (string, error)

GenerateConfigKFromYAML projects a loaded per-env config map (the map[string]any that internal/config.LoadEnvironmentConfig returns from `config.<env>.yaml`) into a `deploy/kcl/<env>/config.k` values file: a typed `app_config: AppConfig = { ... }` instance carrying only the keys the env file actually set.

fields is the proto-derived config field set (same slice the schema and projection emitters consume); projectName is forge.yaml `name`, used only to document the default secret backend in a comment (the ConfigSecretRef defaults themselves live in the generated schema).

func GenerateConfigKScaffold

func GenerateConfigKScaffold(fields []ConfigField, envConfig map[string]any, projectName, kclDirAbs, envName string) (bool, error)

GenerateConfigKScaffold emits deploy/kcl/<envName>/config.k — the per-env, user-owned typed AppConfig VALUES instance migrated from config.<env>.yaml — ONLY when it does not already exist. Returns true when a fresh file was written, false when an existing user-owned file was left untouched.

func GenerateConfigLoader

func GenerateConfigLoader(messages []ConfigMessage, targetDir string, cs *checksums.FileChecksums) error

GenerateConfigLoader generates pkg/config/config.go from parsed config messages.

cs is the project's checksum tracker. Passing it ensures the generated pkg/config/config.go is recorded so `forge audit` doesn't flag it as an orphan. A nil cs is tolerated (file is still written).

func GenerateConfigNativeShared

func GenerateConfigNativeShared(fields []ConfigField, projectName, projectDir, kclDirAbs string, cs *checksums.FileChecksums) error

GenerateConfigNativeShared emits the two project-level, forge-owned KCL files that back the KCL-native config path — <kclDirAbs>/config_schema.k and <kclDirAbs>/config_projection.k — from the proto-derived config fields.

projectDir is the project root (for the checksum-relative path); kclDirAbs is the absolute deploy/kcl directory; cs is the checksum ledger. When cs is nil or the path can't be made relative, the files are still written (untracked).

func GenerateConfigProjectionKCL

func GenerateConfigProjectionKCL(fields []ConfigField) (string, error)

GenerateConfigProjectionKCL emits a KCL file declaring the config projection function (appConfigEnvMap) as a top-level lambda — the idiomatic forge-KCL function form (see kcl/base.k).

The function references the `AppConfig` schema emitted by GenerateConfigSchemaKCL (co-located in the same KCL package) and the `forge.EnvSource` schema from the forge module, so the file carries its own `import forge`.

Fields are visited in proto order. A field with an empty env_var (a component config-block reference, or any field with no env binding) is skipped — mirroring the runtime loader, which binds env only for fields that declare an env_var.

func GenerateConfigSchemaKCL

func GenerateConfigSchemaKCL(fields []ConfigField, projectName string) (string, error)

GenerateConfigSchemaKCL emits a KCL file declaring `schema AppConfig`, one field per proto config field in proto order. Each field's KCL type is mapped from its proto type (see kclTypeForProtoConfig) and its default is projected from the proto annotation (see kclConfigDefaultLiteral). Fields with a description get a leading `#` doc comment.

Component config-block references — a message-typed field whose target message carries its OWN config fields — arrive from forge's descriptor extractor as ProtoType == "message" WITH MessageType set to the block's name (e.g. "TraderConfig") and NO env_var of their own. They carry no scalar KCL type and env binds on the referenced message's leaves, so they are skipped in Phase 1 with a comment marking the omission.

NOTE the discriminator is MessageType, not ProtoType == "message": forge's extractor (internal/cli/forge_descriptor.go) collapses EVERY message kind to ProtoType "message", so a `google.protobuf.Duration` LEAF field also has ProtoType "message" (with GoType "string", MessageType unset, env_var set). Those are real scalar-carried config leaves and MUST be emitted (as `str`), not skipped — keying the skip on MessageType is what tells the two apart.

func GenerateDeployConfig

func GenerateDeployConfig(in DeployConfigGenInput) error

GenerateDeployConfig writes deploy/kcl/<env>/config_gen.k for one environment. It returns nil if there are no config fields at all.

The function is idempotent — running it twice produces the same file.

func GenerateInventory

func GenerateInventory(in InventoryGenInput) error

GenerateInventory emits internal/app/mounts_services.go: the typed per-service Mount<Svc> methods over *Components, the typed MountByName map, MountAll, and the data-only `var Inventory = []ComponentInfo{...}` that introspection (forge map / audit / services listing) reads. It is ALWAYS written when internal/app is emitted (no len(Services)==0 early-return): cmd/server.go references app.Inventory / the typed mounts unconditionally, so the symbols must exist even with no Connect services.

func GenerateK3dPorts

func GenerateK3dPorts(in K3dPortsGenInput) error

GenerateK3dPorts writes deploy/k3d-ports.yaml. Returns nil when there are no listeners — the caller is expected to ensure a stale file is removed in that case (handled in the pipeline gate, not here).

The function is idempotent: same input → byte-identical output. Listeners are sorted by (port, gateway, name) so a hand-rearranged ingress.k doesn't produce a no-op diff.

func GenerateLifecycle

func GenerateLifecycle(in InjectGenInput) error

GenerateLifecycle emits internal/app/lifecycle.go: the supervised- component surface (typed Worker<X>()/Operator<X>() accessors + AllWorkers / AllOperators / HasOperators / RunOperators) over the constructed *Components. Where mounts_services.go is the HTTP surface, this is the worker/operator surface the cmd layer registers onto serverkit.Server. Always written (no len==0 early-return) so cmd/server.go's references resolve even with zero supervised components.

func GenerateMCPManifest

func GenerateMCPManifest(in MCPGenInput) error

GenerateMCPManifest writes gen/mcp/manifest.json. Returns nil with no file written when in.Services is empty — see the package doc for the "tools:[] vs absent file" rationale.

The function is idempotent: identical input produces byte-identical output. Services and methods are emitted in their source order; the caller controls what that order is by passing in the descriptor directly. We do NOT re-sort here because the descriptor's parse order already matches the proto file's declaration order, and shuffling would obscure that.

func GenerateMigrate

func GenerateMigrate(targetDir string, modulePath string, hasMigrations bool, cs *checksums.FileChecksums) error

GenerateMigrate writes pkg/app/migrate.go with embedded migration support. When hasMigrations is true, the generated file includes go:embed directives and golang-migrate logic. When false, AutoMigrate is a no-op stub so that cmd/server.go always compiles.

cs is the project's checksum tracker — both pkg/app/migrate.go and db/embed.go are recorded so `forge audit` doesn't flag drift on them. A nil cs is tolerated.

func GenerateMock

func GenerateMock(svc ServiceDef, mockDir string) (written bool, err error)

GenerateMock generates a mock file for a service. Services with zero RPCs are skipped — there is nothing to mock. Returns (true, nil) if a file was written, (false, nil) if skipped.

func GenerateProviders

func GenerateProviders(modulePath, databaseDriver string, ormEnabled bool, projectDir string) error

GenerateProviders writes internal/app/providers.go ONCE — the owned Infra + OpenInfra (scaffold-once, never overwritten; os.Stat guard below). compose.go (NewComponents) wires each component's Deps INLINE off these Infra fields; the user grows this file as NewComponents reports missing providers.

func GenerateServiceStub

func GenerateServiceStub(svc ServiceDef, targetDir string, crudMethodNames ...map[string]bool) error

GenerateServiceStub generates service.go and handlers.go for a new service using the embedded FS templates. crudMethodNames lists methods that CRUD gen will implement; these are excluded from the initial handlers.go stubs.

func GenerateTenantMiddleware

func GenerateTenantMiddleware(mt *config.MultiTenantConfig, targetDir string, cs *checksums.FileChecksums) error

GenerateTenantMiddleware renders the tenant middleware template and writes the generated file into pkg/middleware/ of the target project. It generates:

  • pkg/middleware/tenant_gen.go (always regenerated — DO NOT EDIT)

cs is the project's checksum tracker; passing it keeps the generated file out of `forge audit`'s orphan list. A nil cs is tolerated.

func GetModulePath

func GetModulePath(dir string) (string, error)

GetModulePath reads the module path from go.mod in the given directory.

func HasExcludeContractDirective

func HasExcludeContractDirective(dir string) bool

HasExcludeContractDirective reports whether the package rooted at dir declares `//forge:exclude-contract` in any of its non-test .go files. A package carrying this directive opts OUT of contract codegen — the per-package equivalent of forge.yaml `contracts.exclude`.

func HasExternalComponentDirective

func HasExternalComponentDirective(dir string) bool

HasExternalComponentDirective reports whether the package rooted at dir declares `//forge:external-component` (or `//forge:provided`) in any of its non-test .go files. A package carrying this directive is skipped by the Build injector (it is hand-wired in providers.go / OpenInfra) but STILL gets its contract/mock codegen.

func HasOptionalDepMarker

func HasOptionalDepMarker(text string) bool

HasOptionalDepMarker returns true if any line in the comment text is *exactly* the `forge:optional-dep` directive (after stripping leading slashes / whitespace and trailing whitespace). The strict match — directive must be the whole line, not embedded inside surrounding prose — prevents documentation that references the marker (e.g. an example block in the scaffolded service.go that says "tag it with the `// forge:optional-dep` marker on the line above") from being interpreted as the marker itself.

Exported so the lint rule (forgeconv-optional-dep-marker-position) can share the exact same recognition logic the parser uses — the rule is "the marker is on a Deps field; anywhere else is a typo / misuse" and the lint check needs to scan for the marker in places it shouldn't be.

IMPORTANT: callers parsing an *ast.CommentGroup should prefer HasOptionalDepMarkerCommentGroup over passing `cg.Text()` here. Go's ast.CommentGroup.Text() silently drops `//directive` form comments (the no-space variant — same shape as `//go:generate`, `//go:noinline`, etc.) which means `//forge:optional-dep` written without a space would be invisible to this function when fed via .Text(). HasOptionalDepMarkerCommentGroup iterates cg.List directly and recognizes both spaced (`// forge:optional-dep`) and unspaced (`//forge:optional-dep`) forms, matching the user's intent.

func HasOptionalDepMarkerCommentGroup

func HasOptionalDepMarkerCommentGroup(cg *ast.CommentGroup) bool

HasOptionalDepMarkerCommentGroup returns true when any comment in the group is the `forge:optional-dep` marker. Unlike HasOptionalDepMarker (which takes the post-.Text() string), this helper inspects the raw c.Text of each *ast.Comment in cg.List, so it recognizes both the spaced form (`// forge:optional-dep`) and the unspaced directive form (`//forge:optional-dep`).

The unspaced form is otherwise invisible to ast.CommentGroup.Text() because Go treats `//<no-space><alnum>` as a compiler/linter directive (same shape as `//go:generate`, `//go:noinline`, `//nolint:...`) and strips it from the joined text. That stripping is a silent footgun for forge users who omit the space — the marker looks present in source but the parser would never see it, so the field would be treated as required, validateDeps would reject nil, and the user's "I marked it optional" intent would be ignored.

Both shapes are accepted; the marker must still be the whole comment (after stripping `//`, `/*`, `*/`, and leading/trailing whitespace). A nil group returns false. Inline /* … */ blocks are honored too — the parser logic is symmetric across both Comment kinds.

func IntrospectComponentNames

func IntrospectComponentNames(projectDir string) []string

IntrospectComponentNames is the flat service-name list (e.g. a "did you mean" hint). Order matches IntrospectComponents.

func IntrospectComponents

func IntrospectComponents(projectDir string) []config.ComponentConfig

IntrospectComponents enumerates a project's Connect SERVICES from the proto descriptor — the single authoritative, non-brittle source (the same one all of codegen reads via ParseServicesFromProtos). It is the shared seam every read-only diagnostic consumer (audit, graph, api, dev info, architecture docs, run, doctor parity, debug) uses instead of the removed components.json manifest.

It deliberately does NOT enumerate workers, operators, or binaries. Those are owned code with no proto contract: the app names them explicitly in its own wiring (lifecycle.go AllWorkers/AllOperators) and enumerates them at runtime. forge has no non-brittle way to know them — a cmd/ filename walk would be exactly the convention-coupled disk read this project rejects — and no need to. Diagnostics therefore report services (with real proto RPC detail); worker/operator inventory is not forge's to synthesize.

Each entry carries only Name, Kind=server, and the conventional handler Path. Ports are a DEPLOY fact (they live in KCL), left unset. Best-effort: a missing or unparsable descriptor yields no services (not an error), so the surface it decorates degrades gracefully rather than failing.

func IsLocallyDeclaredInterface

func IsLocallyDeclaredInterface(typeExpr string, locals map[string]LocalInterface) bool

IsLocallyDeclaredInterface reports whether typeExpr (as printed by printType — a bare identifier or selector) names an interface declared in locals. The Deps field type is matched ignoring surrounding pointer/array decoration: only fields with type "T" where "T" is a local interface name are auto-stubbable. Pointer-to- interface (`*Repository`) is not idiomatic and is left to the hand-roll path.

func NormalizeAuthProvider

func NormalizeAuthProvider(provider string) (normalized string, external bool)

NormalizeAuthProvider canonicalizes a forge.yaml auth.provider value for the cmd-server template: unset/none collapse to "" (no generated auth wiring); api_key/both additionally report external=true (the header-aware generated interceptor owns authentication).

func ParseCRUDOperation

func ParseCRUDOperation(methodName string) (operation, entityName string)

ParseCRUDOperation extracts the CRUD operation and entity name from a method name. Returns ("", "") if the method doesn't match a CRUD pattern. Exported so the CLI's webhook-only detection can ask the same question MatchCRUDMethods does without re-implementing the prefix list. Internal callers should keep using parseCRUDOperation; they're identical.

func ParseLocalInterfaces

func ParseLocalInterfaces(dir string) (map[string]LocalInterface, error)

ParseLocalInterfaces returns every interface type declared in non- test .go files under dir. The set is keyed by interface name so callers can index by the Deps field's pretty-printed type.

Returns an empty map (never nil) and no error if dir doesn't exist — callers treat the absent case the same as "no local interfaces to stub" and fall back to nil for that field.

func ParsePackageClause

func ParsePackageClause(dir string) (string, error)

ParsePackageClause returns the Go package name declared by the .go files directly inside dir (PackageClauseOnly parse — cheap; no type checking, no imports). _test.go files and files starting with "." or "_" are skipped, matching the go tool's build-file rules; external test packages ("foo_test") therefore can't pollute the result.

Errors are diagnostics, not soft fallbacks:

  • dir unreadable or containing no buildable .go file → error telling the user the directory can't be used as a component source dir;
  • files disagreeing on the package clause → error listing each file:line with its declared package so the user can fix the stray clause directly.

func PascalToKebab

func PascalToKebab(s string) string

PascalToKebab converts PascalCase to kebab-case, respecting Go initialisms (LLM, API, URL, JSON, …) so that "LLMGateway" produces "llm-gateway" rather than "l-l-m-gateway".

Thin wrapper around naming.ToKebabCase — kept here for backwards compatibility with existing callers (frontend_pages, frontend_mocks, related tests). New code should call naming.ToKebabCase directly.

func ProtoFileToTSImportPath

func ProtoFileToTSImportPath(protoFile string) string

ProtoFileToTSImportPath converts a proto file path to the TypeScript import path used by the buf ES plugin. For example:

"proto/services/users/v1/users.proto" → "services/users/v1/users_pb"

The buf ES plugin strips the leading proto/ directory and replaces the .proto extension with _pb.

func ProtoTypeToGoType

func ProtoTypeToGoType(protoType string) string

ProtoTypeToGoType converts a proto type to its Go equivalent.

func RegenerateServiceFile

func RegenerateServiceFile(svc ServiceDef, targetDir string) error

RegenerateServiceFile regenerates only service.go for an existing service directory, using the proto-derived HandlerName so that Connect RPC references (Unimplemented*Handler, New*Handler) match the actual proto service name.

func RemoveK3dPorts

func RemoveK3dPorts(projectDir string) error

RemoveK3dPorts deletes deploy/k3d-ports.yaml when present. Used by the generator gate when the ingress feature is off, or when the dev env has no gateways — the previous-generate's file would otherwise become a stale port-mapping the user might forget about.

func ResolveCollisionNaming

func ResolveCollisionNaming(pkg, fallbackFieldName, rolePrefix string, counts map[string]int) (alias, fieldName string)

ResolveCollisionNaming returns the (Alias, FieldName) pair for a component given its raw Package name, a fallback FieldName the caller already computed for the no-collision case, the cross-role collision counts, and the component's role-short-name prefix ("svc", "pkg", "wkr", "op"). When the package collides cross-role, the result is (rolePrefix + Package, RolePrefix + Package) — alias is lower-camel, field name is upper-camel. Otherwise (Package, fallbackFieldName) — preserving the caller's per-role naming convention (services use ToPascalCase; workers/operators also use ToPascalCase so snake_case names produce idiomatic exported identifiers (`Workers.CalibratorRefit` rather than `Workers.Calibrator_refit`); nested packages use a path-encoded form via ToExportedFieldName).

Single source of truth for the wire_gen ↔ bootstrap naming agreement: both files derive their `wireXxxDeps` function name + `Services.Xxx` field reference from this helper, so the two stay in lockstep when a service package collides with an internal-package import.

func ScaffoldOperatorCmd

func ScaffoldOperatorCmd(targetDir, bin, name string) (bool, error)

ScaffoldOperatorCmd is the operator-side analog of ScaffoldWorkerCmd — writes cmd/<bin>/cmd/operators/<name>.go once for a single operator.

func ScaffoldWorkerCmd

func ScaffoldWorkerCmd(targetDir, bin, name string) (bool, error)

ScaffoldWorkerCmd writes the scaffold-once per-worker subcommand file cmd/<bin>/cmd/workers/<name>.go for a SINGLE worker, write-if-absent. This is the `forge add worker` counterpart to the retired generate-time worker loop: one new component, known by name, scaffolded once as OWNED code (the dev then hand-wires it into main.go / lifecycle.go / compose.go). Returns true when it wrote a fresh file (false when one already existed).

func ServiceHasWebhooks

func ServiceHasWebhooks(handlerDir string) bool

ServiceHasWebhooks reports whether a service's directory carries any webhook_<name>.go handler file.

func ServiceNameFromProtoFile

func ServiceNameFromProtoFile(protoFile string) string

ServiceNameFromProtoFile extracts the service name (snake_case) from an entity's proto file path. For example, "proto/services/patients/v1/patients.proto" returns "patients".

func ServiceRowFuncName

func ServiceRowFuncName(svcName string) string

ServiceRowFuncName returns the canonical (no-collision) row constructor name for a service, accepting any spelling the codebase uses (proto "AdminServerService", forge.yaml "admin-server", snake "admin_server"). Used for user-facing messages ("add this line:"); when a cross-role package collision renames the FieldName (rare), the emitted constructor carries the collision-aware name instead and the registration parser's normalized matching still resolves it.

func ToCamelCaseFromPascalExport

func ToCamelCaseFromPascalExport(s string) string

ToCamelCaseFromPascalExport is the exported wrapper around the package-internal helper. Callers outside this package (the frontend hooks barrel generator deriving a namespace alias from a service name) use it so the camelCase rules stay in lockstep across packages.

func VerifyCanKeyUniverse

func VerifyCanKeyUniverse(svc ServiceDef, entities []EntityDef, methods []AuthzMethodData) error

VerifyCanKeyUniverse asserts that every "<action>:<resource>" key a generated CRUD handler will pass to Authorizer.Can exists in the emitted authorizer table. The check recomputes the Can-key set from MatchCRUDMethods (the source of the generated call sites) and compares against the table entries, so any future drift between the two extractions fails `forge generate` loudly instead of shipping an authorizer that warns-and-denies every CRUD request forever.

func WebhookNamesForService

func WebhookNamesForService(handlerDir string) []string

WebhookNamesForService discovers a service's webhooks from the REAL source — the webhook_<name>.go handler files under its directory — rather than a declared config list. `forge add webhook` scaffolds webhook_<name>.go (+ _test.go + a shared webhook_store.go); the file IS the declaration, so nothing needs to cache the name in forge.yaml or a components manifest. This mirrors how forge discovers services themselves.

handlerDir is the service's on-disk directory (e.g. internal/handlers/foo, resolved via ResolveServiceComponent). Returns the webhook names sorted for stable codegen; webhook_store.go and *_test.go are excluded. Best-effort: a missing directory yields nil.

Types

type AppField

type AppField struct {
	Name string
	Type string

	// Placeholder is the target type the AppExtras field should be
	// tightened to once a sibling lane lands the real type. Set when
	// the field carries a `// forge:placeholder: <Type>` doc/inline
	// comment marker. Empty for fields without the marker.
	//
	// The annotation exists because parallel agents writing into
	// app_extras.go often need to declare a field whose typed Repository /
	// Client / Provider lives in a sibling lane that hasn't merged yet.
	// Typing the field as `any` keeps the build green; the placeholder
	// marker says "I know this is `any` — wire_gen should treat me as
	// <Type>, and lint should error until I'm tightened to <Type>".
	//
	// wire_gen renders a typed `resolve<Field>(app)` accessor when the
	// marker is present so call sites can consume the field as the
	// promised type rather than `any`. When the user finally tightens
	// the field declaration from `any` to <Type>, the accessor stays
	// correct (the type assertion is a no-op for a value already typed
	// as <Type>).
	Placeholder string
}

AppField describes one exported field on the project-generated *App struct (pkg/app/bootstrap.go) plus any user-extension files in the same package. wire_gen consumes these to resolve unconventional service Deps fields by name → app.<Field>.

Type is parallel to DepsField.Type so callers can do an exact string-equal match when they want to be conservative, or a contains check when they want to tolerate alias differences.

func ParseAppFields

func ParseAppFields(appDir string) ([]AppField, error)

ParseAppFields walks every non-test .go file in pkg/app and returns the union of fields reachable as `app.<Name>` on a *App. The set includes:

  • Direct exported fields of the `App` struct itself (forge-owned in pkg/app/app_gen.go: Services, Workers, Operators, Packages, DB, ORM).
  • Direct exported fields of the `AppExtras` struct (user-owned in pkg/app/app_extras.go). AppExtras is embedded into App as a pointer; Go's field promotion rules make those fields reachable via `app.<Field>` at the call site even though they live on a different struct. wire_gen treats them identically.

AppExtras fields may carry a `// forge:placeholder: <Type>` doc or inline comment marker, which surfaces as AppField.Placeholder. See the AppField docstring for the why.

Anonymous (embedded) fields on App are skipped from the result — they don't have a usable selector name on their own, and the only embedded type we generate (*AppExtras) is unwrapped above.

Returns an empty slice if pkg/app doesn't exist yet (initial scaffold path) — caller treats that as "wire_gen has nothing to look up by name" and falls back to the conventional set.

type AppFieldRef

type AppFieldRef struct {
	DepsField string // e.g. "Repo"
}

AppFieldRef pairs a package Deps field name with the app.<name> expression bootstrap should emit for it. Only emitted when the AppExtras field type EXACTLY matches the Deps field type (otherwise the compile fails with funding.Repository vs *db.PostgresRepository style mismatches).

type AuthTemplateData

type AuthTemplateData struct {
	Provider    string // "jwt", "api_key", "both"
	JWT         config.JWTConfig
	APIKey      config.APIKeyConfig
	Module      string
	SkipMethods []string // procedure names that don't require auth
}

AuthTemplateData holds the data shape expected by the auth middleware templates.

type AuthzMethodData

type AuthzMethodData struct {
	Procedure     string   // full RPC procedure path, e.g. "/services.users.v1.UserService/CreateUser"
	RequiredRoles []string // roles that grant access (empty = any authenticated user)
	AuthRequired  bool     // whether auth is required for this method
	// AuthzCustom marks a method whose authorization is delegated to a
	// hand-written authorizer ((forge.v1.method).authz_custom). It carries no
	// role allow-list, so the template must NOT emit it with empty roles (that
	// reads as an any-authenticated grant). Instead the template emits it
	// FAIL-CLOSED — a sentinel "custom — see interceptor" role that no caller
	// holds — so the generated table can't be misread as a grant. The real
	// decision is enforced by the descriptor-driven RoleInterceptor + the
	// service's authorizer.go, never this table.
	AuthzCustom bool
	// Errors records the Connect/gRPC error codes the method may return,
	// derived from (forge.v1.method).errors. The template emits a
	// per-method entry in `methodErrors` so handler readers (including
	// LLMs) see the typed error contract alongside the role table.
	// Methods with no declared errors are omitted from the map.
	Errors []string
}

AuthzMethodData holds per-method authorization metadata for the authorizer template.

func BuildAuthzMethods

func BuildAuthzMethods(svc ServiceDef, entities []EntityDef) []AuthzMethodData

BuildAuthzMethods converts a service's RPC methods into the authorizer-table entries the template emits. It returns BOTH key spellings of the policy universe:

  • one entry per RPC keyed by the Connect procedure path (checked by CanAccess from the auth middleware), and
  • one alias entry per CRUD-matched RPC keyed by "<action>:<resource>" (e.g. "create:patient") — the exact keys the generated CRUD handler bodies pass to Can. The alias carries the same required-roles/auth-required flags as its underlying RPC.

The aliases are derived from MatchCRUDMethods — the SAME extraction crud_gen uses to emit the Can() call sites — so every generated Can key exists in the generated table by construction. VerifyCanKeyUniverse re-checks that invariant independently at generate time.

type AuthzTemplateData

type AuthzTemplateData struct {
	Package     string            // Go package name, e.g. "users"
	ServiceName string            // proto service name, e.g. "UserService"
	Module      string            // Go module path
	Methods     []AuthzMethodData // per-method authorization data
}

AuthzTemplateData holds the data shape expected by authorizer_gen.go.tmpl.

type BootstrapComponentData

type BootstrapComponentData struct {
	Name       string // e.g. "api", "cache", "email_sender"
	Package    string // e.g. "api" (Go package identifier — leaf of ImportPath for nested entries)
	ImportPath string // e.g. "api" or "mcp/database" (path under internal/, workers/, etc.)
	FieldName  string // e.g. "API" or "McpDatabase" (exported struct field, must be unique)
	VarName    string // e.g. "api" or "mcpDatabase" (unique lowerCamel local-var prefix in bootstrap)
	Fallible   bool   // true if New() returns (T, error)
	// Alias is the import alias used in bootstrap.go for this component's
	// Go package. Defaults to Package when there are no cross-role
	// collisions; gets a role-prefixed value (e.g. "svcBilling",
	// "pkgBilling") when a service Package matches an internal package
	// Package (or other cross-role pair). All bootstrap.go references
	// to the package's exported symbols must use Alias rather than
	// Package so the alias-rewrite is observed at every call site.
	Alias string
	// HasWebhooks is true when this service has webhooks declared in
	// forge.yaml. The bootstrap template uses this to emit a
	// `RegisterWebhookRoutes(mux, stack)` call after `RegisterHTTP(...)`,
	// so generated webhook routes get mounted on the mux without the user
	// having to hand-edit the user-owned `RegisterHTTP` body in
	// handlers/<svc>/service.go. Only populated for services; ignored
	// for packages, workers, and operators.
	HasWebhooks bool
	// HasLogger / HasConfig record whether this component's Deps struct
	// declares a Logger / Config field. The bootstrap template gates the
	// emission of those Deps-literal fields on these flags so a package
	// that doesn't consume them isn't forced to carry vestigial Logger /
	// Config fields just to keep the generated New(Deps{...}) call site
	// type-checking. Populated by inspectComponentDepsShape before
	// rendering; defaults to false (skip) when the source dir can't be
	// parsed (e.g. just-scaffolded component with no Deps yet).
	HasLogger bool
	HasConfig bool
	// AppFieldRefs lists Deps fields (other than Logger/Config) whose
	// names AND types match an AppExtras field. Bootstrap emits one
	// assignment per entry like `<DepsField>: app.<DepsField>`. Without
	// this, audit.New got only {Logger} even when audit.Deps.Repo and
	// app.Repo both existed — the package silently degraded (Log warn-
	// and-drops) until the next forge generate cycle. wire_gen has had
	// this logic for services; this brings packages to parity.
	AppFieldRefs []AppFieldRef
	// CanonicalAppField names the single App/AppExtras field whose
	// declared type is this internal package's Service interface (e.g.
	// AppExtras.DaemonService of type svcdaemon.Service). Populated by
	// inspectComponentDepsShape ONLY when the package's Deps struct has
	// at least one non-optional collaborator field that bootstrap cannot
	// auto-wire from App/AppExtras (no name match, or proven type
	// mismatch) — i.e. the construction is unexpressible from app
	// fields. When set, the bootstrap template emits
	//
	//	app.Packages.<FieldName> = app.<CanonicalAppField>
	//
	// instead of `<pkg>.New(<pkg>.Deps{...})`: user-owned setup.go
	// constructs the canonical, fully-wired instance (with deps that
	// have no AppExtras representation — inline URL builders,
	// env-derived strings, cross-package collaborators), and appkit
	// runs Setup BEFORE the package table, so the alias always observes
	// the setup.go assignment. Constructing a second instance here
	// instead would produce a half-built duplicate that panics in
	// validateDeps or silently no-ops — the cp-forge svcdaemon
	// hand-edit class (Deps.DaemonRepo/URLBuilder unwireable, boot
	// panic "Deps.DaemonRepo is required").
	//
	// Empty when: every Deps field auto-wires (keep constructing — the
	// enforcement/Checker shape), the only gaps are config scalars
	// (zero value is the documented degraded mode — the billing/APIKey
	// shape), the gap is `forge:optional-dep`-marked, no App/AppExtras
	// field has the package's Service type, or more than one does
	// (ambiguous — no deterministic canonical instance).
	CanonicalAppField string
	// ConnectPkg is the import alias of the generated Connect package for
	// this service (e.g. "echov1connect") — used by the bootstrap template
	// to reference the `<X>ServiceName` constant when building vanguard
	// REST services. Only populated for services and only when
	// `api.rest: true`; empty for non-services or when REST is off.
	ConnectPkg string
	// ProtoServiceName is the PascalCase proto service identifier (e.g.
	// "EchoService") used to look up the `<ProtoServiceName>Name` constant
	// in the connect-generated package. Combined with ConnectPkg, the
	// bootstrap template emits `<connectPkg>.<ProtoServiceName>Name` as
	// the Connect path passed to vanguard.NewService.
	ProtoServiceName string
}

BootstrapComponentData represents a bootstrappable component (service, package, worker, operator).

For nested internal packages (e.g. internal/mcp/database/contract.go), Package is the leaf Go-identifier ("database"), while ImportPath carries the full path under internal/ ("mcp/database") used to construct the import line. For top-level packages, ImportPath is the same as Package. FieldName must remain unique across all packages, so for nested entries it should encode the full path (e.g. "McpDatabase") to avoid collisions with sibling leaves. VarName is the lowerCamel form of FieldName and is used in the bootstrap template as a unique prefix for local Go variables (e.g. "mcpDatabaseImpl"); using FieldName avoids collisions when two nested packages share a leaf name.

type BootstrapOperatorData

type BootstrapOperatorData = BootstrapComponentData

func OperatorDataFromNames

func OperatorDataFromNames(names []string, projectDir string) ([]BootstrapOperatorData, error)

OperatorDataFromNames builds BootstrapOperatorData from operator names (e.g. from forge.yaml). Thin wrapper over OperatorDataFromSpecs preserved for callers without forge.yaml context. See WorkerDataFromNames for the disk-first resolution + FieldName rationale (same snake_case → PascalCase rule).

func OperatorDataFromSpecs

func OperatorDataFromSpecs(specs []OperatorSpec, projectDir string) ([]BootstrapOperatorData, error)

OperatorDataFromSpecs is the operator-side analog of WorkerDataFromSpecs — honors `path:` when set so operator dirs with separator-bearing leaves (e.g. `operators/cert_rotator`) get the correct import line. See WorkerDataFromSpecs for the disk-first path/alias/error rules.

type BootstrapPackageData

type BootstrapPackageData = BootstrapComponentData

func PackageDataFromNames

func PackageDataFromNames(names []string, projectDir string) ([]BootstrapPackageData, error)

PackageDataFromNames builds BootstrapPackageData from package names (e.g. from forge.yaml or discoverPackages). Names may be flat ("cache") or nested using forward slashes ("mcp/database"). For nested names the leaf segment is the Go package identifier (used at call sites like `database.New(...)`), while the full path is preserved for the import line and for deriving a unique FieldName/VarName so two leaves with the same name (e.g. "mcp/database" and "foo/database") don't collide in generated code.

projectDir is the root project directory; if non-empty, it is used to detect fallible constructors by inspecting the Go source in internal/<importPath>/, and — disk-first — to read the leaf package's REAL package clause so the generated alias/selector can never disagree with what internal/<importPath> actually declares (a snake_case dir like internal/email_sender may declare either `package email_sender` or `package emailsender`; only the file on disk knows which). Synthesis of the leaf name applies only when the directory doesn't exist (e.g. unit tests passing bare name lists).

Returns an error when the package directory exists but its package clause is unparseable or self-conflicting — see ParsePackageClause.

type BootstrapServiceData

type BootstrapServiceData = BootstrapComponentData

Type aliases for backward compatibility and readability.

type BootstrapTestServiceData

type BootstrapTestServiceData struct {
	Name    string // e.g. "api"
	Package string // e.g. "api" (Go package CLAUSE — may differ from the dir name)
	// ImportPath is the handler directory leaf under handlers/ as it exists
	// ON DISK (e.g. "engine_shadow"), resolved by ResolveServiceComponent.
	// The testing.go template builds its import line from this, never from
	// Package — a dir may legally declare a package name that differs from
	// its directory name, and only the directory name belongs in the path.
	ImportPath             string
	FieldName              string // e.g. "API" (exported struct field)
	ProtoServiceName       string // e.g. "ApiService" (proto service name for connect client)
	ProtoConnectImportPath string // e.g. "github.com/foo/bar/gen/services/api/v1/apiv1connect"
	ProtoConnectPkg        string // e.g. "apiv1connect" (Go identifier used at call sites)
	Fallible               bool   // true if New() returns (T, error)
	HasDB                  bool   // true if Deps struct has a DB orm.Context field
	// HasAuthorizer is true when the service's Deps struct declares an
	// `Authorizer` field. The test harness only wires the permissive test
	// authorizer (deps.Authorizer = cfg.authz + the AuthzInterceptor in
	// NewTest<Svc>Server) for services that actually carry that dep. A
	// carve-out / `//forge:external-component` / shared-descriptor-authz
	// service has no Authorizer dep, so emitting deps.Authorizer for it is a
	// compile error — this gate omits it (mirrors inventory_gen's HasAuthorizer
	// and the run-path Mount<Svc>, which only thread the authz interceptor
	// when the service declares the dep). Same signal both halves read: the
	// presence of a Deps field named "Authorizer".
	HasAuthorizer bool
	// Alias mirrors BootstrapComponentData.Alias — when an internal package
	// shares its leaf-name with this service's package, both get role-prefixed
	// aliases ("svcBilling" vs "pkgBilling") so the generated testing.go imports
	// don't collide.
	Alias string
	// VarName is the lowerCamel form of FieldName, used as the testConfig
	// field name (e.g. `c.billingDeps`). Defaults to lowerFirst(Package);
	// becomes "svcBilling" when there's a cross-role collision so the
	// services-range and packages-range testConfig fields stay distinct.
	VarName string
	// AutoStubs lists the per-Deps-field synthesized interface stubs the
	// template should emit and inject as defaults inside NewTest<Svc>.
	// Each entry corresponds to a Deps field whose Go type is an interface
	// declared locally in the handler package (so the testing.go file can
	// reference the interface via the imported handler package alias).
	// Optional-dep fields are excluded — those stay nil to preserve the
	// "graceful dev-mode degrade" semantics. See ParseLocalInterfaces +
	// HasOptionalDepMarker for the detection rules.
	AutoStubs []DepsAutoStub
	// UnresolvedStubs lists Deps fields whose type is a cross-package
	// selector forge couldn't resolve (alias not in imports, package
	// can't load, type isn't an interface). The template emits a
	// `// TODO: stub <type>` line next to NewTest<Svc> so the user
	// sees a visible reminder to hand-roll an override via
	// With<Svc>Deps(...). Empty when every selector resolved cleanly.
	UnresolvedStubs []UnresolvedAutoStub
}

BootstrapTestServiceData holds data for a single service in the bootstrap testing template.

ProtoConnectImportPath and ProtoConnectPkg are derived from the proto file's declared `go_package` rather than from the service name. This makes the generated testing.go correct even when the proto file's go_package doesn't follow the convention `<module>/gen/services/<svc>/v1/<svc>v1` (for example when multiple proto services live in a single proto file and share one go package, or when the package name has a custom alias).

type BootstrapTestingGenInput

type BootstrapTestingGenInput struct {
	GenContext

	Services           []ServiceDef
	Packages           []BootstrapPackageData
	Workers            []BootstrapWorkerData
	Operators          []BootstrapOperatorData
	MultiTenantEnabled bool
}

GenerateBootstrapTesting generates pkg/app/testing.go from the bootstrap_testing.go.tmpl template.

cs is the project's checksum tracker — passing it keeps pkg/app/testing.go recorded so `forge audit` doesn't flag stale state on it. A nil cs is tolerated.

BootstrapTestingGenInput embeds GenContext (ProjectDir / ModulePath / Checksums) and adds the component inventory + multi-tenancy toggle. Replaces the prior 8-positional-parameter signature; field names map 1:1 to the old params.

type BootstrapWorkerData

type BootstrapWorkerData = BootstrapComponentData

func WorkerDataFromNames

func WorkerDataFromNames(names []string, projectDir string) ([]BootstrapWorkerData, error)

WorkerDataFromNames is the legacy entry point — thin wrapper over WorkerDataFromSpecs with empty Path. Preserved for callers (and tests) that don't carry forge.yaml context. New code should use WorkerDataFromSpecs so the explicit `path:` field is honored.

FieldName derives from the ORIGINAL name (which retains its separators) via ToPascalCase so snake_case worker names still produce idiomatic exported identifiers (`Workers.CalibratorRefit`, `wireWorkerCalibratorRefitDeps`) rather than the run-together `Workers.Calibratorrefit` shape.

func WorkerDataFromSpecs

func WorkerDataFromSpecs(specs []WorkerSpec, projectDir string) ([]BootstrapWorkerData, error)

WorkerDataFromSpecs builds BootstrapWorkerData honoring each spec's optional `path:` field. When Path is set, the on-disk dir leaf is used for the import line — so snake_case dirs like `workers/climatology_refresh/` produce the import line `workers/climatology_refresh` — while the Go package alias still comes from the directory's REAL `package X` clause when the dir exists (disk-first; see disk_resolver.go).

When Path is empty, the worker's EXISTING directory + package clause are resolved from disk (ResolveComponentDir), so `engine_shadow` keeps importing workers/engine_shadow with whatever package name that directory actually declares. Synthesis (`naming.GoPackage`'s snake_case canonical form) applies only when the directory doesn't exist yet — i.e. a brand-new scaffold.

Returns an error when a worker directory exists but its package clause is unparseable/ambiguous (see ParsePackageClause) — guessing here is exactly the broken-imports bug class disk-first resolution eliminates.

Cross-role collision (worker named `audit` vs internal/audit/) is still resolved by AssignBootstrapAliases prefixing one of the colliding aliases.

type BuildComponent

type BuildComponent struct {
	// Name is the runtime kebab name (display / inventory selection only —
	// never a construction key). Used for stable sort + diagnostics.
	Name string
	// FieldName is the exported Go field name on the *Services struct
	// (e.g. "Billing", "SvcBilling"). The collision-aware name shared
	// with the inventory.
	FieldName string
	// VarName is the lower-camel local variable name build.go binds the
	// constructed instance to (e.g. "billing").
	VarName string
	// Alias is the import alias for the component's package.
	Alias string
	// ImportPath is the module-relative import path
	// (e.g. "internal/handlers/billing").
	ImportPath string
	// ServiceTypeKey is the type-identity key this component PRODUCES —
	// the thing a consumer's Deps field type is matched against to draw an
	// edge. It is the FULL IMPORT-PATH-qualified Service interface
	// (e.g. "example.com/proj/internal/billing.Service"), NOT the bare
	// package clause: two packages may share a clause name (a domain
	// `internal/billing` and a handler `internal/handlers/billing` both
	// `package billing`), and keying by clause would collide them, mis-
	// wiring a consumer's domain dep to the handler instance. Import paths
	// are unique, so import-path keying gives each package a distinct
	// identity. Empty for components that expose no collaborator interface
	// (pure leaf workers).
	//
	// The clause-qualified form is retained as a FALLBACK lookup
	// (compPackageKey) for the unambiguous single-clause case + mid-edit
	// projects where the consumer's import block can't be parsed.
	ServiceTypeKey string
	// Deps are the parsed Deps fields, in declaration order.
	Deps []DepsField
	// contains filtered or unexported fields
}

BuildComponent is one node in the construction graph: a registered service / worker / operator, its parsed Deps fields, and the metadata the build.go scaffold needs to emit its constructor call.

type BuildEdge

type BuildEdge struct {
	Consumer string // consumer FieldName
	Producer string // producer FieldName
	Field    string // the Deps field on the consumer that carries the dep
	Type     string // the declared field type (for comments / setter stubs)
}

BuildEdge records a resolved consumer -> producer dependency: the consumer's Deps field Field (typed Type) was matched to the producer component. Used both to order construction and to render each constructor call's collaborator assignments.

type BuildPlan

type BuildPlan struct {
	// Order is the topo-sorted construction order (producers first). When
	// a cycle exists, Order contains the acyclic prefix and Cycles names
	// the components that could not be ordered.
	Order []BuildComponent
	// Edges are every resolved consumer->producer collaborator edge,
	// including back-edges that participate in a cycle (see CycleEdges).
	Edges []BuildEdge
	// Cycles lists the FieldNames of components left unordered because
	// they sit on a dependency cycle. Empty for a clean DAG.
	Cycles []string
	// CycleEdges are the specific edges build.go must break with a
	// two-phase setter stub: each is a consumer->producer edge where both
	// endpoints are in Cycles. The user completes the setter after both
	// instances exist.
	CycleEdges []BuildEdge
}

BuildPlan is the result of ordering the construction graph.

func ComputeBuildPlan

func ComputeBuildPlan(comps []BuildComponent, resolver TypeResolver) BuildPlan

ComputeBuildPlan orders comps so every component is constructed after the components it depends on by type. resolver decides, per Deps field, whether the field's type is produced by another registered component (an edge) or is a conventional/external dep (no edge).

Determinism: comps are processed in a stable order (by FieldName) and Kahn's queue is kept sorted, so the emitted build.go is byte-stable across regenerates regardless of map iteration order upstream.

func (BuildPlan) HasCycle

func (p BuildPlan) HasCycle() bool

HasCycle reports whether the plan contains an unresolved dependency cycle. forge map / audit use this as a guardrail signal.

type CRUDMethod

type CRUDMethod struct {
	Method    MethodTemplateData // The RPC
	Entity    EntityDef          // The matched entity
	Operation string             // "create", "get", "list", "update", "delete"
}

CRUDMethod holds the correlation between an RPC method and a database entity.

func MatchCRUDMethods

func MatchCRUDMethods(svc ServiceDef, entities []EntityDef) []CRUDMethod

MatchCRUDMethods correlates a service's RPC methods with entity definitions and returns the matched CRUD methods. Only unary RPCs are matched.

type CRUDMethodTemplateData

type CRUDMethodTemplateData struct {
	MethodName        string // "CreatePatient"
	InputType         string // "CreatePatientRequest"
	OutputType        string // "CreatePatientResponse"
	EntityName        string // "Patient"
	EntityLower       string // "patient"
	Operation         string // "create", "get", "list", "update", "delete"
	AuthRequired      bool
	AuthAction        string // "create", "read", "list", "update", "delete" (middleware constant)
	PkField           string // "Id" (proto PascalCase Go field name)
	PkColumnName      string // "id" (raw DB column name)
	PkGoType          string // "int64"
	HasPkInInput      bool   // true if the request message likely has an ID field
	ResponseField     string // "Patient" — the proto field name in the response that holds the entity
	HasPagination     bool   // true when List method's InputType follows AIP-158 convention
	PaginationStyle   string // "cursor" (default for now)
	HasFilters        bool   // true if list method has filter fields
	FilterFields      []FilterFieldData
	HasOrderBy        bool   // true if list method has order_by field
	HasTenant         bool   // true when the entity has a tenant key field
	TenantGoName      string // e.g., "OrgId", "TenantId" (PascalCase Go field name on entity)
	TenantColumnName  string // e.g., "org_id", "tenant_id"
	UpdateEntityField string // e.g., "Project" — Go field name in the update request that holds the entity
	// UpdateMaskField is the Go field name of the update request's
	// google.protobuf.FieldMask field (e.g. "UpdateMask"). Empty when the
	// request carries no mask — the generated UpdateOp then omits
	// Mask/PersistMasked and pkg/crud keeps the legacy full-replace path.
	// When set, the op wires both hooks so HandleUpdate honors AIP-134:
	// concrete mask paths write only the named columns via
	// db.Update<Entity>Masked.
	UpdateMaskField string
	CreateFields    []CreateFieldData // fields from the create request message
	// ShapeMismatch is true when the request/response message shapes
	// observed in svc.Messages don't line up with what the CRUD body
	// templates assume (AIP-158 page_size/page_token for list, an `id`
	// scalar key for get/update/delete, an entity-typed response field,
	// etc.). That's a legitimate domain decision, not an error — the
	// template emits a tagged stub returning CodeUnimplemented rather
	// than CRUD-body code that wouldn't compile against the real proto,
	// and the user implements the custom shape in the owned shim. See
	// validateCRUDShape for the rules. The stub carries a
	// `forge:custom-read-shape` marker plus MismatchReason so the user
	// (and `forge audit`) can spot it. (Markers emitted before this
	// release spelled it FORGE_CRUD_SHAPE_MISMATCH; audit still
	// recognizes that string for one release.)
	ShapeMismatch  bool
	MismatchReason string
	// CustomFilters are the request fields that DID map to a declared
	// column on the entity (best-effort), used to seed the wired
	// custom-read-shape body's []orm.QueryOption skeleton. Unlike the
	// strict FilterFields (which fail the generate on an unmappable
	// field), these are advisory: fields that don't map to a column are
	// silently skipped so the scaffold always compiles. Only populated
	// when ShapeMismatch is true.
	CustomFilters []FilterFieldData
	// CreateAssigns are the precomputed `e.X = req.X` statements that
	// map create-request fields onto the entity struct (with timestamp/
	// wrapper/array/width conversions baked in).
	CreateAssigns []string
}

CRUDMethodTemplateData holds per-method template data.

type CRUDTemplateData

type CRUDTemplateData struct {
	Package       string // Go package name, e.g. "patients"
	Module        string // Go module path, e.g. "github.com/demo-project"
	ProtoPackage  string // e.g. "proto/services/patients"
	DBPackagePath string // e.g. "github.com/demo-project/gen/db/v1"
	HasPagination bool   // true if any list method uses pagination
	HasFilters    bool   // true if any list method has filter fields
	HasOrderBy    bool   // true if any list method has order_by
	NeedsORM      bool   // true if pagination, filters, or ordering requires orm import
	HasTenant     bool   // true if any CRUD method operates on a tenant-scoped entity
	// NeedsCRUDLib is true when at least one method emits a real CRUD
	// body (i.e. uses pkg/crud, internal/db, middleware). When every
	// method's request/response shape failed validation and we emit
	// only TODO stubs, the template skips those imports to keep the
	// file compiling.
	NeedsCRUDLib bool
	// NeedsOpsFile is true when the Tier-1 ops file must be written:
	// either NeedsCRUDLib (a real op constructor) OR there is at least
	// one entity conversion pair a wired custom-read-shape body depends
	// on. An all-custom service still needs the conversion helpers, so
	// it gets an ops file carrying only the <entity>ToProto/FromProto
	// pairs (no crud/middleware imports — see the template gating).
	NeedsOpsFile bool
	CRUDMethods  []CRUDMethodTemplateData
	// Entities carries the per-entity proto<->struct conversion pairs
	// (<entity>ToProto / <entity>FromProto) emitted alongside the ops.
	Entities []EntityConvTemplateData
	// NeedsTimestamppb gates the timestamppb import (set when any
	// conversion touches a timestamp column).
	NeedsTimestamppb bool
}

CRUDTemplateData holds all data needed to render the CRUD handlers template.

type CRUDTestEntityData

type CRUDTestEntityData struct {
	EntityName       string // "Patient"
	EntityLower      string // "patient"
	PkField          string // "Id"
	PkGoType         string // "int64"
	HasCreate        bool
	HasGet           bool
	HasList          bool
	HasUpdate        bool
	HasDelete        bool
	HasAllCRUD       bool   // true if all 5 operations exist
	HasTenant        bool   // true when the entity has a tenant key field
	TenantGoName     string // e.g., "OrgId"
	TenantColumnName string // e.g., "org_id"
	HasTimestamps    bool   // entity annotation timestamps:true — created_at is asserted set
	// MutableStringField is the Go name of the first non-PK string field
	// (e.g. "Name") — the field the lifecycle test mutates to prove
	// update actually writes. Empty when the entity has none.
	MutableStringField string
	// MutableStringFieldPath is MutableStringField's proto field name
	// (snake_case) — the AIP-134 update_mask path for it.
	MutableStringFieldPath string
	// SecondStringField/-Path name a SECOND mutable string field when the
	// entity has one (skipping the tenant key). The masked-update test
	// loads it with a clobber value the mask does NOT name, then asserts
	// it survived — proving the mask restricts the write. Empty when the
	// entity has only one string field (the non-clobber assertion is then
	// skipped; the masked write itself is still exercised).
	SecondStringField     string
	SecondStringFieldPath string
	CreateMethod          CRUDMethodTemplateData
	GetMethod             CRUDMethodTemplateData
	ListMethod            CRUDMethodTemplateData
	UpdateMethod          CRUDMethodTemplateData
	DeleteMethod          CRUDMethodTemplateData
	Fields                []CRUDTestFieldData // entity proto message fields (minus PK, minus deleted_at)
	CreateFields          []CRUDTestFieldData // fields from the CreateRequest message
	UpdateEntityField     string              // Go field name holding entity in UpdateRequest, e.g. "Project"
}

CRUDTestEntityData groups CRUD operations by entity for lifecycle tests.

type CRUDTestFieldData

type CRUDTestFieldData struct {
	ProtoName string    // "Name"
	GoType    string    // "string"
	Kind      FieldKind // scalar, enum, message, wrapper, timestamp, etc.
	TestValue string    // `"test-value"` or `1` or `true`
}

CRUDTestFieldData holds per-field data for generating test values.

type CRUDTestTemplateData

type CRUDTestTemplateData struct {
	Package      string                   // Go package name, e.g. "patients"
	Module       string                   // Go module path, e.g. "github.com/demo-project"
	ProtoPackage string                   // e.g. "proto/services/patients"
	HasTenant    bool                     // true if any entity has tenant isolation
	Entities     []CRUDTestEntityData     // Grouped per-entity test data
	CRUDMethods  []CRUDMethodTemplateData // All CRUD methods (for individual error tests)
	// NeedsFieldMask gates the fieldmaskpb import: true when at least one
	// entity's lifecycle test emits the AIP-134 masked-update block.
	NeedsFieldMask bool
	// TestHelperName mirrors ServiceTemplateData.TestHelperName: the suffix
	// the bootstrap testing generator emits on `app.NewTest<X>` /
	// `app.NewTest<X>Server`. CRUD test scaffolds use this rather than
	// pascal-casing Package so the call site matches the actual factory
	// when an internal package shares the service's leaf name.
	TestHelperName string
}

CRUDTestTemplateData holds all data needed to render the CRUD test template.

type CmdCtorRef

type CmdCtorRef struct {
	Ctor string // e.g. "NewAuditLogCmd"
}

CmdCtorRef is one group-command constructor reference for the composition root — the exported New<X>Cmd func name the main.go template qualifies with its group package (services./workers./operators.).

type CmdGroupItem

type CmdGroupItem struct {
	// Module is the project module path (for the import lines).
	Module string

	// Bin is the primary binary name — the cmd/<bin>/cmd import path segment
	// the group file imports for the shared Deps/Serve helpers.
	Bin string

	// Name is the runtime kebab-case component name — the cobra Use value and
	// the <name>.go filename stem. Identical derivation to the app inventory
	// row Name and the typed Mount<Svc> / Worker<X>() / Operator<X>() accessors.
	Name string

	// FieldName is the exported PascalCase suffix used in the generated
	// command constructor (New<FieldName>Cmd) and self-registration. It is
	// the PLAIN per-role name (ToPascalCase of the trimmed service name) —
	// the constructor is a local symbol in the group package, so it is not
	// subject to the cross-role collision rename. Workers/operators set
	// FieldName == MountFieldName (no Components-field mount, no collision).
	FieldName string

	// MountFieldName is the exported suffix of the typed mount METHOD the
	// service command calls: (*app.Components).Mount<MountFieldName>. For
	// services it MUST equal the name inventory_gen emitted — which is
	// collision-aware (ResolveCollisionNaming), so a service whose handler
	// package collides cross-role with an internal package mounts as
	// MountSvc<Pkg>, not Mount<Pkg>. Keeping this SEPARATE from FieldName lets
	// the constructor stay New<Plain>Cmd while the mount call names the
	// collision-renamed Components method — matching inventory_gen exactly and
	// killing the MountBilling/MountSvcBilling mismatch (BUG 1). For
	// workers/operators it is set equal to FieldName (their template doesn't
	// reference a Components mount method).
	MountFieldName string
}

CmdGroupItem is one generated command-group entry — one cmd/<bin>/cmd/<group>/<name>.go file that self-registers via init(). It serves services, workers, and operators alike.

type CmdMainTemplateData

type CmdMainTemplateData struct {
	Module    string
	Bin       string
	Services  []CmdCtorRef
	Workers   []CmdCtorRef
	Operators []CmdCtorRef
}

CmdMainTemplateData feeds cmd-main.go.tmpl — the composition root. main.go is no longer a thin blank-import + cmd.Execute(); it names EVERY group constructor explicitly and passes them to cmd.Execute. That makes main.go inventory-dependent (like the per-component group files), so it is rendered here from the SAME service/worker/operator rows the group files are — not from the project-level scaffold data — which is why GenerateCmdGroups owns it and the upgrade managed-file list does not.

type CmdServerTemplateData

type CmdServerTemplateData struct {
	Module       string
	ConfigFields map[string]bool

	// AuthProvider is the normalized forge.yaml auth.provider ("jwt",
	// "api_key", "both"; empty when unset/none). Non-empty emits the
	// InstallGeneratedAuth call in runServer.
	AuthProvider string

	// AuthProviderExternal is true for header-carried providers
	// (api_key/both): the generated header-aware interceptor joins the
	// project chain and the authn layer runs in passthrough.
	AuthProviderExternal bool

	// RESTEnabled mirrors forge.yaml `api.rest: true`. When set the cmd
	// composition site builds a vanguard transcoder over the mounted
	// services' Connect paths and serves it in place of the bare mux.
	// Filled by generateCmdServerData from projectAPIRESTEnabled.
	RESTEnabled bool
}

CmdServerTemplateData holds the data passed to cmd-server.go.tmpl. It combines project-level data (Module) with config field awareness so the template can conditionally include code that references specific config fields, plus the forge.yaml auth provider so the generated runServer actually CALLS the generated auth wiring (middleware.InstallGeneratedAuth) instead of leaving it decorative.

type CmdServiceGroupInput

type CmdServiceGroupInput struct {
	Bin      string
	Services []string               // raw service-name spellings
	Packages []BootstrapPackageData // internal packages — for cross-role collision counts
}

CmdServiceGroupInput drives GenerateCmdGroups: the primary binary name plus the SERVICE rows (the SAME proto-derived rows the app mount surface is generated from, so every service subcommand lines up with a typed (*app.Components).Mount<Svc>). Workers/operators are deliberately absent — generate performs no worker/operator discovery; their subcommands are scaffold-once OWNED code written by `forge add worker/operator`.

type CmdServicesTemplateData

type CmdServicesTemplateData struct {
	Skipped []string // runtime names skipped for colliding with built-ins
}

CmdServicesTemplateData feeds cmd-svc-register.go.tmpl (the services group anchor + collision NOTEs).

type ConfigBlockRef

type ConfigBlockRef struct {
	FieldName string // root Config field, e.g. "Trader"
	TypeName  string // generated struct type, e.g. "TraderConfig"
}

ConfigBlockRef names one component config block as composed on the root Config: the Config field holding it and the generated Go type. wire_gen consumes this (via ConfigBlocksFromMessages) to resolve Deps fields of a block type to `cfg.<FieldName>` by TYPE.

func ConfigBlocksFromMessages

func ConfigBlocksFromMessages(messages []ConfigMessage) []ConfigBlockRef

ConfigBlocksFromMessages derives the component config-block references from parsed config messages: every message-typed field on a root config message whose MessageType names another config message in the set. Order follows root-message declaration order, so consumers get a deterministic candidate list.

type ConfigField

type ConfigField struct {
	Name         string // Proto field name (e.g., "database_url")
	GoName       string // Go field name (e.g., "DatabaseUrl")
	GoType       string // Go type (e.g., "string", "int32", "bool")
	ProtoType    string // Proto type (e.g., "string", "int32", "bool")
	EnvVar       string // From config_field.env_var
	Flag         string // From config_field.flag
	DefaultValue string // From config_field.default_value
	Required     bool   // From config_field.required
	Description  string // From config_field.description
	Sensitive    bool   // From config_field.sensitive — projects to a Secret in deploy
	Category     string // From config_field.category — groups fields in deploy gen

	// Role is the (forge.v1.config).role annotation as the bare enum spelling
	// (e.g. "CONFIG_FIELD_ROLE_MODE"; "" for UNSPECIFIED). Config codegen
	// keys semantic behavior (Mode()/DevAuthBypass()) on THIS, never on the
	// field NAME — so renaming a field never changes behavior, and naming a
	// field "environment" without the annotation never auto-enables dev mode.
	// `json:",omitempty"` keeps old descriptors readable (additive contract).
	Role string `json:",omitempty"`

	// MessageType names the referenced config message when this field is
	// a component config-block reference (ProtoType == "message"), e.g. a
	// root `AppConfig` field `TraderConfig trader = 21;` records
	// MessageType "TraderConfig". Empty for scalar fields. Block-reference
	// fields carry no env_var/flag of their own — env binding lives on the
	// referenced message's leaf fields. `json:",omitempty"` keeps old
	// descriptors readable (additive contract, see audit-json skill).
	MessageType string `json:",omitempty"`
}

ConfigField represents a single field in a config proto message with ConfigFieldOptions annotations.

type ConfigMessage

type ConfigMessage struct {
	Name   string        // Message name (e.g., "AppConfig")
	Fields []ConfigField // Fields with config_field annotations
}

ConfigMessage represents a parsed config proto message.

func DefaultConfigMessages

func DefaultConfigMessages() []ConfigMessage

DefaultConfigMessages returns the default scaffold config metadata used before protoc-gen-forge has produced a descriptor for proto/config/config.proto.

func ParseConfigProto

func ParseConfigProto(protoPath string) ([]ConfigMessage, error)

ParseConfigProto reads config messages from the forge descriptor, filtering to those from a specific proto file path.

func ParseConfigProtosFromDir

func ParseConfigProtosFromDir(dir string) ([]ConfigMessage, error)

ParseConfigProtosFromDir reads all config messages from the forge descriptor. Falls back to empty if the descriptor does not exist yet.

type ConfigTemplateBlockField

type ConfigTemplateBlockField struct {
	GoName   string // field on Config, e.g. "Trader"
	TypeName string // block struct type, e.g. "TraderConfig"
}

ConfigTemplateBlockField is one block-typed field on the root Config struct (e.g. `Trader TraderConfig`).

type ConfigTemplateBlockType

type ConfigTemplateBlockType struct {
	TypeName string
	Fields   []ConfigTemplateField
}

ConfigTemplateBlockType is one component config-block struct type the template declares alongside Config (e.g. `type TraderConfig struct`). Deduped by TypeName when the same block message is referenced by more than one root field.

type ConfigTemplateData

type ConfigTemplateData struct {
	// Fields is every leaf field — root fields plus component config-block
	// leaves — in declaration order, with GoPath set. Drives RegisterFlags
	// and Load so block leaves get the exact same env/flag/default
	// treatment as root fields.
	Fields []ConfigTemplateField
	// RootFields are the leaves declared directly on Config (struct decl).
	RootFields []ConfigTemplateField
	// BlockTypes / BlockFields carry the component config-block shapes:
	// the struct type declarations and the Config fields holding them.
	BlockTypes  []ConfigTemplateBlockType
	BlockFields []ConfigTemplateBlockField
	// RoleModeField is the Go field name of the field tagged
	// role=CONFIG_FIELD_ROLE_MODE, or "" when no field carries it. The
	// generated Mode()/DevAuthBypass() read THIS field — selected by
	// annotation, never by the name "Environment". Renaming the role field
	// is a behavior no-op; naming an unannotated field "environment" never
	// enables dev mode.
	RoleModeField string
	NeedsStrconv  bool

	// Module is the project's Go module path, used by config.go.tmpl to
	// import the generated proto config package (gen/config/v1). Set by
	// GenerateConfigLoader; left empty by callers that only need the
	// field partition (e.g. ConfigBlocksFromMessages).
	Module string
}

ConfigTemplateData is the top-level data passed to the config.go template.

func BuildConfigTemplateData

func BuildConfigTemplateData(messages []ConfigMessage) ConfigTemplateData

BuildConfigTemplateData partitions parsed config messages into the template shape:

  • Block messages — those referenced by a MessageType field of another config message — become nested struct types (`type TraderConfig struct`) plus a typed field on Config (`Trader TraderConfig`).
  • Every other message's scalar fields flatten onto the root Config struct exactly as before (most projects have a single AppConfig).

Block leaves keep their own env_var/flag/default annotations and join the flat Fields list with a qualified GoPath, so env binding, flag registration, and per-env deploy projection all reuse the existing flat plumbing unchanged.

One nesting level is supported: message-typed fields ON a block message are ignored. References to messages that aren't in the set (or carry no config fields) are skipped.

type ConfigTemplateField

type ConfigTemplateField struct {
	GoName         string
	GoType         string
	EnvVar         string
	Flag           string
	DefaultValue   string
	Description    string
	Required       bool
	HasDefault     bool
	DefaultInt32   int32
	DefaultInt64   int64
	DefaultBool    bool
	DefaultFloat32 float32
	DefaultFloat64 float64

	// GoPath is the assignment path on the generated Config struct.
	// Root fields: identical to GoName ("Port"). Component config-block
	// leaves: qualified through the block field ("Trader.MaxPerTick").
	// The Load/flag plumbing in config.go.tmpl assigns via GoPath so one
	// flat loop covers both shapes.
	GoPath string

	// IsDuration marks duration-shaped string fields (see
	// isDurationField). They are emitted as time.Duration on the Config
	// struct and parsed ONCE in Load — consumers never re-parse strings,
	// and a typo'd duration fails startup instead of silently zeroing.
	IsDuration bool

	// StructGoType is the Go type emitted on the Config struct:
	// "time.Duration" for duration fields, GoType for everything else.
	StructGoType string

	// ParseFn names the parse helper Load feeds to loadField for this
	// field ("parseString", "parseInt32", "parsePort", "parseGoDuration",
	// …). Selecting it at generate time keeps the emitted Load a flat
	// list of identical one-liners.
	ParseFn string

	// AllowEmptyEnv preserves the historical string semantics: a string
	// env var explicitly set to "" counts as set. Numeric/bool/duration
	// fields treat an empty env var as unset (parsing "" would always
	// error).
	AllowEmptyEnv bool

	// Role is the (forge.v1.config).role annotation (bare enum spelling, ""
	// for none). Codegen selects semantic fields (e.g. the MODE field) by
	// THIS, never by the field's name.
	Role string

	// Sensitive mirrors (forge.v1.config).sensitive. Sensitive fields get NO
	// CLI flag (defense against shell-history / `ps` leaks) and are resolved
	// from env / Secret mount only — never a flag or an inline default.
	Sensitive bool
}

ConfigTemplateField holds template data for a single config field.

type CreateFieldData

type CreateFieldData struct {
	ProtoGoName  string    // Go field name on the proto request message, e.g. "Name"
	EntityGoName string    // Go field name on the ORM entity, e.g. "Name"
	Kind         FieldKind // scalar, enum, message, wrapper, timestamp, etc.
	GoType       string    // Go type: "string", "int32", "*timestamppb.Timestamp", etc.
	EnumGoType   string    // For enum fields: the pb.EnumType name
}

CreateFieldData holds a field mapping from a create request to the ORM entity.

type CrossPkgInterfaceResult

type CrossPkgInterfaceResult struct {
	// PackagePath is the import path of the package declaring the
	// interface (e.g. "example.com/proj/internal/repo").
	PackagePath string
	// PackageName is the package's declared Go name (the qualifier
	// the testing.go file should use for the interface, e.g. "repo").
	PackageName string
	// Methods is the flattened method set of the interface, with each
	// method's parameter / result types fully qualified using package
	// aliases that appear in NeededImports.
	Methods []InterfaceMethod
	// NeededImports maps every extra import path the stub's method
	// signatures reference to the local alias used in the rendered
	// signatures. The interface's own package is INCLUDED here so
	// callers can fold one map into the file's import block.
	NeededImports map[string]string
}

CrossPkgInterfaceResult bundles everything the auto-stub emitter needs to satisfy a selector-typed Deps field. ImportPath is the canonical import path of the package declaring the interface (so the testing.go imports block can pick up a new entry). NeededImports lists every extra import path the stub's method signatures reference (e.g. an `*orm.Context` parameter contributes orm's path). NeededImports is map[importPath]alias, where alias is the suggested local alias (typically the package's declared name).

func ResolveCrossPkgInterface

func ResolveCrossPkgInterface(handlerDir, pkgAlias, typeName string) (CrossPkgInterfaceResult, bool)

ResolveCrossPkgInterface attempts to locate `<pkgAlias>.<typeName>` — where pkgAlias was imported by some file in handlerDir — and return the data the bootstrap_testing.go template needs to emit a satisfying stub struct.

Returns ok=false on any failure mode (alias not found, package can't load, type isn't an interface, etc.). The caller should treat ok=false the same as "no stub possible" and skip the field — the generated testing.go will leave it nil and rely on the user overriding via With<Svc>Deps when a test cares.

Implementation outline:

  1. Parse every non-test, non-_gen.go file in handlerDir to build a map alias -> importPath. We take the FIRST alias declaration we see; in practice each package is imported with one alias per file and aliases agree across files in a well-formed package.
  2. Resolve pkgAlias to an import path.
  3. Use golang.org/x/tools/go/packages to load that path, with cfg.Dir = handlerDir so module resolution works (go.mod in the project root governs the lookup).
  4. Look the named type up in the package's types scope. Reject anything not an interface.
  5. Walk the interface's method set (which Go's types package pre-flattens — embedded methods are included automatically).
  6. Render each method's signature with types.TypeString plus a custom Qualifier that records every package the signature references AND returns the right alias for the rendered text.

type DeployConfigGenInput

type DeployConfigGenInput struct {
	GenContext

	ProjectName string         // forge.yaml `name`
	EnvName     string         // dev / staging / prod / ...
	KCLDir      string         // deploy/kcl (absolute or relative)
	Fields      []ConfigField  // proto-derived config fields (with annotations)
	EnvConfig   map[string]any // per-env config values loaded from the sibling config.<env>.yaml file
}

DeployConfigGenInput is the per-env input for GenerateDeployConfig. Embeds GenContext for ProjectDir + Checksums (ModulePath is unused — KCL output carries no Go imports). ProjectDir may be empty here for callers that pass an absolute KCLDir outside the project tree; the checksum path then falls back to the raw path (see GenerateDeployConfig).

type Deps

type Deps struct{}

Deps is the dependency set for the codegen Services. Empty today; the package owns its template imports directly.

type DepsAssignabilityMatcher

type DepsAssignabilityMatcher struct {
	// contains filtered or unexported fields
}

DepsAssignabilityMatcher answers "is AppExtras.<FieldName> assignable to Deps.<FieldName>?" for a given roleRoot (e.g. "internal", "handlers", "workers", "operators") and package directory under it.

One instance per generate run. Methods are safe for concurrent use within a single generate (the cache mutex serializes loads).

Construction is cheap: NewDepsAssignabilityMatcher only stores the project dir. Packages are loaded lazily on the first Match call that requires them. A project that's missing pkg/app or whose source doesn't type-check still constructs fine and reports MatchUnavailable (→ wire-the-name-match, the pre-matcher behavior).

func NewDepsAssignabilityMatcher

func NewDepsAssignabilityMatcher(projectDir string) *DepsAssignabilityMatcher

NewDepsAssignabilityMatcher returns a matcher rooted at projectDir. projectDir is the directory containing go.mod / pkg/app / handlers / workers / operators / internal.

func (*DepsAssignabilityMatcher) AssignablePairs

func (m *DepsAssignabilityMatcher) AssignablePairs(roleRoot, pkgDir string) []InterfaceAssertion

AssignablePairs returns the proven concrete→interface satisfaction pairs for one component, ready to emit as `var _ <Interface> = (<Concrete>)(nil)` assertions. For each Deps field of the component whose declared type is an INTERFACE and whose name-matched App/AppExtras field holds a POINTER-or-INTERFACE concrete that the type checker proves satisfies it, one assertion is produced.

Why only interface Deps fields with pointer/interface concretes:

  • The whole value of the assertion (FORGE_SHAPE_REDESIGN §6c) is making "what implements <Interface>" greppable. That only applies when Deps.<F> IS an interface and the wired concrete is a distinct named type (the fat-repo case: *db.PostgresRepository satisfies a dozen narrow per-service Repository interfaces with no assertion).
  • `(<Concrete>)(nil)` is a guaranteed-valid zero only for pointer and interface concretes. Value-typed collaborators (rare for the repo/client shape this targets) are skipped rather than risk an un-compilable zero expression.

Pairs where the concrete and interface are identical (Deps field typed as the same interface the App field already holds) are skipped — the assertion would be a tautology.

Returns nil (no assertions, no error) when the component's universe could not be loaded/type-checked — assertions are a best-effort greppability aid, never a generate-blocking gate.

func (*DepsAssignabilityMatcher) Match

func (m *DepsAssignabilityMatcher) Match(roleRoot, pkgDir, depsFieldName, depsTypeStr, appTypeStr string, appNameKnown bool) MatchKind

Match resolves whether the named Deps field should be wired from AppExtras. roleRoot is "internal" / "handlers" / "workers" / "operators"; pkgDir is the directory name under roleRoot (e.g. "audit", "billing", possibly nested like "mcp/database"); depsFieldName is the Go field name on the package's Deps struct; appNameKnown reports whether AppExtras has a same-name field (as parsed by ParseAppFields — the cheap AST path); depsTypeStr / appTypeStr are the pretty-printed type strings the legacy matchers already had.

The matcher uses the cheap inputs to avoid a go/types load when the answer is unambiguous (no name match, or byte-equal strings). It only loads packages when the strings differ AND a name match exists — exactly the case the legacy compare got wrong.

type DepsAutoStub

type DepsAutoStub struct {
	// FieldName is the Deps field name as declared (e.g. "Repo").
	FieldName string
	// StubType is the unqualified Go identifier the template should use
	// for the synthesized stub struct (e.g. "stubApiRepo"). Generated
	// from the service alias + field name so two services with the same
	// Deps-field name don't collide at the package level.
	StubType string
	// InterfaceQualified is the package-qualified type expression used
	// when injecting the stub into Deps in NewTest<Svc>.
	//
	// For locally-declared interfaces this carries the literal "<alias>."
	// placeholder so the caller can substitute the post-collision
	// service alias ("svcBilling" vs "billing"). For cross-package
	// interfaces (CrossPackage = true) the prefix is already the
	// declaring package's alias (e.g. "repo.Repository") and must NOT
	// be re-aliased — the service alias is irrelevant to it.
	InterfaceQualified string
	// Methods are the interface's flattened method set rendered for
	// the template's stub-emit loop.
	Methods []InterfaceMethod
	// CrossPackage flags stubs whose interface lives in a package
	// other than the handler's. The caller uses this to skip the
	// "<alias>." rewrite and to fold the stub's ExtraImports into
	// the file's import block.
	CrossPackage bool
	// ExtraImports lists every package the stub's method signatures
	// reference (including the interface's own package). Only populated
	// when CrossPackage = true. The bootstrap_testing assembler
	// deduplicates these across stubs into the top-level
	// ExtraImports field on the template data.
	ExtraImports []ExtraImport
}

DepsAutoStub describes one synthesized interface implementation emitted into the generated pkg/app/testing.go for a service-owned Deps field. The stub satisfies the field's interface with zero-value returns; it exists so NewTest<Svc>(t) can construct the Service even when the field is required by validateDeps. Tests that exercise real behavior continue to override via With<Svc>Deps(...).

type DepsField

type DepsField struct {
	// Name is the exported Go field name as written in the Deps struct,
	// e.g. "Logger", "Config", "Authorizer", "Repo", "Audit", "DB".
	Name string

	// Type is the pretty-printed Go type expression, e.g. "*slog.Logger",
	// "*config.Config", "middleware.Authorizer", "orm.Context", "*sql.DB",
	// "Repository". Used by wire_gen to emit zero-values when no
	// producer matches and to render TODO comments that name the type
	// the user needs to wire.
	Type string

	// Optional is true when the field's doc / inline comment carries
	// the `// forge:optional-dep` marker. Optional fields are
	// intentionally allowed to be nil at construction time:
	//   - validateDeps should NOT enforce them (the user manages
	//     `if s.deps.X != nil { ... }` per RPC as idiomatic Go).
	//   - wire_gen emits the typed zero silently — no TODO comment,
	//     no contribution to the UNRESOLVED header — when no producer
	//     matches.
	// The marker exists because some Deps fields are legitimately
	// optional (rollback-only NATS publisher, optional gateway
	// features, etc.) and the default "must resolve" treatment forces
	// users to either fake-wire them or drop them from validateDeps
	// entirely. Both defeat the design intent.
	Optional bool
}

DepsField describes one field of a service's `Deps` struct as parsed from handlers/<svc>/service.go (or any non-test .go file in the dir).

The wire_gen codegen consumes these to emit one assignment per field in the per-service `wireXxxDeps(app, cfg)` function. Type is the pretty-printed Go expression (selector / star / ident / etc.) so downstream consumers can do simple string contains checks (e.g. "*sql.DB", "orm.Context") without re-walking the AST.

func ParseServiceDeps

func ParseServiceDeps(dir string) ([]DepsField, error)

ParseServiceDeps reads handlers/<svc>/<*>.go (skipping test files) and returns the ordered list of fields declared on the package-level `Deps` struct. Returns an empty slice if the directory doesn't exist or no Deps struct is found — caller treats those as "service has no rich deps to wire".

Modeled on DetectDepsDBField (same fast AST walk, same skip-test-files behavior). Kept as a separate function rather than overloaded so DetectDepsDBField stays a constant-time predicate that doesn't have to allocate a slice.

type EntityColumn

type EntityColumn struct {
	Name string // column name, snake_case
	// Type is the canonical type: "string", "int64", "float64",
	// "bool", "time", "json", "bytes" (matches schemadef.CanonicalType).
	Type    string
	IsArray bool
	NotNull bool
	IsPK    bool
	// DeclType is the declared SQL type verbatim ("TIMESTAMPTZ").
	DeclType string `json:",omitempty"`
	Default  string `json:",omitempty"`
	// IsGenerated marks GENERATED ALWAYS AS (...) STORED columns — the DB
	// computes them, so the ORM must never write them (Bun's ,scanonly).
	IsGenerated bool `json:",omitempty"`
}

EntityColumn is one introspected column of an entity's table.

type EntityConvTemplateData

type EntityConvTemplateData struct {
	EntityName       string   // "Item"
	EntityLower      string   // "item"
	ToProtoAssigns   []string // statements: "m.Name = e.Name"
	FromProtoAssigns []string
}

EntityConvTemplateData renders one entity's conversion pair.

func BuildEntityConv

func BuildEntityConv(svc ServiceDef, entity EntityDef) EntityConvTemplateData

BuildEntityConv builds the conversion data for one entity.

type EntityDef

type EntityDef struct {
	Name      string        // "Patient"
	TableName string        // "patients"
	PkField   string        // "id"
	PkGoType  string        // "string"
	Fields    []EntityField // wire-message fields (service proto)
	ProtoFile string        // proto file declaring the wire message
	// Columns is the introspected applied schema for the entity's table.
	Columns []EntityColumn `json:",omitempty"`
	// SearchColumns are the text columns the generated list search
	// filter matches against (convention: every text column).
	SearchColumns    []string `json:",omitempty"`
	SoftDelete       bool     `json:",omitempty"`
	Timestamps       bool     `json:",omitempty"`
	HasTenant        bool     // true when the table has a tenant_id column
	TenantFieldName  string   // proto field name: "tenant_id"
	TenantGoName     string   // Go name: "TenantId"
	TenantColumnName string   // DB column: "tenant_id"
}

EntityDef is a database entity: the join of an introspected table from the APPLIED schema (db/migrations executed against the shadow DB — the storage truth) with the service-proto CRUD message shape (the wire truth).

Columns drive the ORM and entity structs; Fields drive the frontend and the proto<->entity conversion in the CRUD wiring. SoftDelete / Timestamps / HasTenant are conventions read off real columns (deleted_at, created_at+updated_at, tenant_id) — never annotations.

func BuildSchemaEntities

func BuildSchemaEntities(projectDir string, services []ServiceDef) ([]EntityDef, error)

BuildSchemaEntities is the entity source of truth: it joins the APPLIED schema (db/migrations shadow-applied and introspected) with the service protos' CRUD method shapes.

An entity exists when BOTH halves exist:

  • a service declares CRUD RPCs for it (Create<X>/Get<X>/List<Xs>/...), giving the wire message shape, and
  • the applied schema has the matching table (pluralized snake_case of the entity name), giving columns/PK/conventions.

CRUD RPCs without a table generate nothing (the honest-routes contract: no pages, no ORM, no nav for entities that don't exist). Tables without CRUD RPCs are plain schema — owned by hand-written code, invisible to the CRUD/frontend projections.

func ParseEntityProtos

func ParseEntityProtos(projectDir string) ([]EntityDef, error)

ParseEntityProtos returns the project's entities. Despite the historical name, entities are no longer parsed from proto annotations: they are the join of the APPLIED schema (db/migrations shadow-applied + introspected) with the service protos' CRUD method shapes — see BuildSchemaEntities. Falls back to empty when the descriptor or migrations don't exist yet.

type EntityField

type EntityField struct {
	Name      string    // Proto field name: "patient_id"
	GoName    string    // Go name: "PatientId"
	ProtoType string    // "int64", "string", etc.
	GoType    string    // "int64", "string", etc.
	Kind      FieldKind // scalar, enum, message, etc.
	IsFK      bool
	FKTable   string // "patients" (if FK)
	// MessageType carries the fully-qualified message name for
	// message-typed fields (e.g. "google.protobuf.Timestamp"). ProtoType
	// collapses these to "message", which made every timestamp column
	// degrade to TEXT in plan-based migrations/ORM. Additive.
	MessageType string `json:",omitempty"`
}

EntityField represents a single field in an entity.

type EntityPageField

type EntityPageField struct {
	Name    string // camelCase TS field name: "createdAt"
	Label   string // display label: "Created At"
	IsBadge bool   // render as a status Badge (enum kind or enum-like string name)
}

EntityPageField is one renderable entity field for list columns / detail rows.

type ExtraImport

type ExtraImport struct {
	Path  string
	Alias string
}

ExtraImport is a single rendered import line for the bootstrap_testing.go template's extra-imports block. Carries an explicit alias even when it matches the path's leaf, so the template can emit `<alias> "<path>"` uniformly — Go tolerates the redundant alias and it makes the template logic one line shorter.

func SortedNeededImports

func SortedNeededImports(needed map[string]string) []ExtraImport

SortedNeededImports turns the unordered map produced by ResolveCrossPkgInterface into a deterministic ordered slice. Used by the bootstrap_testing.go data assembly so generated imports are stable across runs (deterministic codegen — required for the checksums-based "no spurious diff" guarantee).

type FieldKind

type FieldKind string

FieldKind classifies a proto field for code generation branching.

const (
	FieldKindScalar          FieldKind = "scalar"
	FieldKindEnum            FieldKind = "enum"
	FieldKindMessage         FieldKind = "message"
	FieldKindMap             FieldKind = "map"
	FieldKindRepeatedScalar  FieldKind = "repeated_scalar"
	FieldKindRepeatedMessage FieldKind = "repeated_message"
	FieldKindWrapper         FieldKind = "wrapper"   // google.protobuf.*Value
	FieldKindTimestamp       FieldKind = "timestamp" // google.protobuf.Timestamp
)

func DetermineFieldKind

func DetermineFieldKind(protoType, goType string) FieldKind

DetermineFieldKind classifies a field based on its ProtoType and GoType.

type FilterFieldData

type FilterFieldData struct {
	ProtoName  string // e.g., "active", "search", "status"
	GoName     string // PascalCase: "Active", "Search", "Status"
	ColumnName string // DB column: "active", "status"
	FieldType  string // "bool", "string", "int32", "int64"
	FilterType string // "exact", "search"
	IsOptional bool   // proto optional keyword
	// SearchColumns is the entity's declared string columns (minus the
	// PK) that a "search" filter spans via orm.WhereILikeAny. A search
	// field never maps to a column of its own — the historical
	// WhereILike("search", ...) hit a phantom column and either errored
	// or (SQLite double-quote fallback) silently matched nothing.
	SearchColumns []string
}

FilterFieldData describes a filter field extracted from a List request message.

type ForgeDescriptor

type ForgeDescriptor struct {
	Services []ServiceDef    `json:"services"`
	Configs  []ConfigMessage `json:"configs"`
}

ForgeDescriptor is the JSON structure written by protoc-gen-forge --mode=descriptor.

type FrontendHookMethod

type FrontendHookMethod struct {
	Name       string // PascalCase: "GetUser"
	NameCamel  string // camelCase: "getUser"
	InputType  string // "GetUserRequest"
	OutputType string // "GetUserResponse"
	IsQuery    bool   // true for Get/List/Search, false for mutations
	// EntityScope is the camelCase singular CRUD entity this method
	// operates on ("task" for ListTasks/GetTask/CreateTask), derived
	// from the RPC-name CRUD pattern. Empty for non-CRUD methods.
	// Queries embed it in their query key ([service, entity, method,
	// req]); mutations invalidate the [service, entity] scope when set,
	// falling back to the whole-service scope when empty (a bespoke
	// mutation may touch anything, so over-invalidating is the safe
	// default there).
	EntityScope string
}

FrontendHookMethod represents a single unary RPC method for hook generation.

type FrontendHookTemplateData

type FrontendHookTemplateData struct {
	ServiceName      string // e.g., "UserService"
	ServiceNameCamel string // e.g., "userService"
	ImportPath       string // e.g., "services/users/v1/users_pb" — the service's own proto file
	Methods          []FrontendHookMethod
	// HasQueries / HasMutations let the template conditionally import
	// only the hooks it actually uses. Without these flags the emitted
	// file pulls in useMutation/useQueryClient/UseMutationOptions for
	// query-only services, tripping no-unused-vars in eslint configs.
	HasQueries   bool
	HasMutations bool
	// SchemaImports groups input-message `Schema` value imports by their
	// declaring proto file's TS import path. The template emits one
	// `import { ...Schema }` statement per entry. Same-file schemas land
	// under the service's ImportPath; cross-file schemas land under their
	// own proto file's derived path. Sorted for deterministic output.
	SchemaImports []HookImportGroup
	// TypeImports groups output-message `type` imports the same way.
	// Kept separate from SchemaImports because the template emits these
	// as `import type { ... }` — value vs type-only is required so a
	// `--isolatedModules` build still tree-shakes the type-only side.
	TypeImports []HookImportGroup
	// Workspaces is true when the project opted into the pnpm-workspace
	// layout (frontend.workspaces: true). When true, the rendered hook
	// file lives under packages/hooks/src/generated/ and imports
	// connectClient from "../transport" + proto types from the
	// project's @<scope>/api workspace. When false (the default), the
	// file lives under frontends/<name>/src/hooks/ and imports from the
	// frontend-local @/lib/connect + @/gen paths — byte-identical to
	// projects that predate the workspaces flag.
	Workspaces bool
	// ApiPackage is the workspace package name for the shared API
	// (e.g. "@myapp/api"). Empty when Workspaces is false.
	ApiPackage string
	// EntityScopes is the sorted, deduplicated set of camelCase CRUD
	// entity names ("task", "user") derived from the service's RPC
	// names. The template emits one entity-scope key per entry in the
	// generated query-key factory so mutations can invalidate ONLY the
	// queries for the entity they touched (entity-scoped invalidation)
	// instead of nuking every query on the service.
	EntityScopes []string
}

FrontendHookTemplateData holds data for rendering a single service's TypeScript React Query hooks file.

func ServiceDefToHookData

func ServiceDefToHookData(svc ServiceDef) FrontendHookTemplateData

ServiceDefToHookData converts a ServiceDef to FrontendHookTemplateData, skipping streaming RPCs.

type GenContext

type GenContext struct {
	// ProjectDir is the project root. Output paths are computed relative
	// to it, and it's the base for the relative paths recorded in
	// Checksums. Some env-scoped emitters tolerate an empty ProjectDir
	// when their output lives outside the project tree (see the emitter's
	// own doc); the core bootstrap emitters require it.
	ProjectDir string

	// ModulePath is the Go module path (e.g.
	// "github.com/acme/control-plane"), used to build import lines and
	// derive resource names (leader-election lease, etc.). Empty for
	// emitters that don't render Go imports (k3d ports, deploy config).
	ModulePath string

	// Checksums, when non-nil, records each rendered Tier-1 file's hash
	// so `forge audit` doesn't flag forge-owned output as stale. A nil
	// tracker is tolerated everywhere — the file is still written.
	Checksums *checksums.FileChecksums
}

GenContext is the project-scoped context shared by every emitter's GenInput. Embed it; do not pass it around on its own.

type HookImportGroup

type HookImportGroup struct {
	ImportPath string   // e.g., "services/users/v1/users_pb" or "shared/v1/types_pb"
	Symbols    []string // sorted, deduplicated identifiers
}

HookImportGroup is one TS import statement: a list of symbols (sorted, deduplicated) drawn from a single source proto file. The template emits one statement per group so cross-proto-file refs resolve to the declaring _pb.ts file.

type InfraAssignabilityMatcher

type InfraAssignabilityMatcher struct {
	// contains filtered or unexported fields
}

InfraAssignabilityMatcher answers "which Infra field fills this Deps field type?" for a component. One instance per generate run; methods are safe for concurrent use within a generate (the cache mutex serializes loads).

func NewInfraAssignabilityMatcher

func NewInfraAssignabilityMatcher(projectDir string) *InfraAssignabilityMatcher

NewInfraAssignabilityMatcher returns a matcher rooted at projectDir (the directory containing go.mod / internal/app / internal/handlers / ...).

func (*InfraAssignabilityMatcher) ResolveInfraField

func (m *InfraAssignabilityMatcher) ResolveInfraField(roleRoot, pkgDir, depsFieldName, depsType string, infraFields map[string]InfraField) (string, MatchKind)

ResolveInfraField returns the Infra field name that should fill the named Deps field, plus the MatchKind classifying the proof:

  • MatchAssignable — an Infra field is PROVEN assignable to depsType.
  • MatchExactString — an exact-name Infra field has a byte-equal type string (no go/types load needed).
  • MatchUnavailable — an exact-name Infra field exists but assignability is unproven; emit it as the compile-time backstop.
  • MatchUnprovenBackstop — NO assignable Infra field was found, AND the absence cannot be PROVEN because the universe is mid-write (the Deps field type or some Infra field type did not type-check this run). The caller emits the compile-time backstop `infra.<DepsField>` rather than raising a spurious generate-time MissingProvider — this is the generate-ORDERING fix (see deps_assignability.go's MatchUnprovenBackstop doc). The returned field name is the Deps field name (the compile-time backstop target); the compiler arbitrates whether it exists.
  • (empty field, MatchNoName) — no Infra field fills this type AND the Infra surface was seen COMPLETELY (proven negative); the caller raises MissingProvider for a required collaborator.

Priority: an exact-name Infra field whose type is byte-equal (fast path); then any provably-assignable Infra field (the narrow-interface case), with a deterministic pick when several are assignable; then an exact-name Infra field as the unproven backstop; then the unproven/proven negative split.

type InfraField

type InfraField struct {
	Name string
	Type string
}

InfraField is one exported field on the owned *Infra struct, parsed from internal/app via AST (the cheap path — matches ParseAppFields). Type is the pretty-printed declared type for the exact-string fast path.

type InjectAssignment

type InjectAssignment struct {
	Field   string
	Expr    string
	Comment string
}

InjectAssignment is one `Field: Expr,` line in a New(Deps{...}) literal.

type InjectComponentData

type InjectComponentData struct {
	// FieldName is the exported field on *Services (e.g. "Billing",
	// "SvcBilling") — shared with the inventory and bootstrap naming.
	FieldName string
	// VarName is the lower-camel base name (e.g. "billing"); used for the
	// per-component authz var (`<VarName>Authz`).
	VarName string
	// LocalVar is the local variable Build binds the constructed instance
	// to — VarName + "Inst" so it never shadows the package import alias.
	LocalVar string
	// Alias is the import alias for the component's package.
	Alias string
	// ImportPath is the module-relative import path (e.g.
	// "internal/handlers/billing").
	ImportPath string
	// Package is the Go package clause (for the constructor selector and
	// doc comments).
	Package string
	// Fallible reports whether New returns (T, error). Build wraps the
	// error with the component name when true; assigns directly otherwise.
	Fallible bool
	// NeedsAuthzVar is true when the Deps struct declares an Authorizer
	// field. Build emits a `var authz` block with the dev-bypass swap.
	NeedsAuthzVar bool
	// Assignments are the per-Deps-field key/value pairs, in Deps
	// declaration order, for the New(Deps{...}) literal.
	Assignments []InjectAssignment
}

InjectComponentData is one component's rendered inputs for the NewComponents construction body: the import line, the constructor selector, and the ordered Deps-literal assignments resolved by type.

type InjectGenData

type InjectGenData struct {
	Module            string
	NeedsAuthorizer   bool
	NeedsConfigImport bool
	// NeedsFmt gates the `fmt` import: it is only referenced in the fallible
	// (New returns error) construction branch, so a project with no fallible
	// component (incl. the zero-component case) must not import it or the
	// generated file fails to compile on an unused import.
	NeedsFmt bool
	// Fields is the Components struct field set (one per component, typed as
	// its concrete handler/worker/operator type), in stable FieldName order.
	Fields []composeField
	// Order is the topo-sorted construction sequence (producers first).
	Order []InjectComponentData
	// HasCycle / CycleEdges drive the two-phase setter stub block.
	HasCycle   bool
	CycleEdges []BuildEdge
}

InjectGenData is the rendered template input for compose.go.tmpl.

type InjectGenInput

type InjectGenInput struct {
	GenContext
	Services  []ServiceDef
	Packages  []BootstrapPackageData
	Workers   []BootstrapWorkerData
	Operators []BootstrapOperatorData
}

InjectGenInput carries everything GenerateInject needs to assemble the component set. Mirrors the bootstrap inputs so the two derive identical FieldName / alias values (one source of truth: ResolveCollisionNaming).

func (InjectGenInput) RoleRoot

func (in InjectGenInput) RoleRoot(c BuildComponent) string

RoleRoot returns the role-root directory the assignability matcher loads the component's package from, keyed by the component's role. The role is encoded on the assembled BuildComponent (compRoleRoot).

type Inspector

type Inspector interface {
	DetectFallibleConstructor(dir string) (bool, error)
	DetectDepsDBField(dir string) (bool, error)
}

Inspector walks user-project Go source to detect constructor shape and dependency fields. Used by bootstrap generation to decide between fallible / infallible wiring.

func NewInspector

func NewInspector(_ Deps) Inspector

NewInspector constructs the Go AST inspector surface.

type InterfaceAssertion

type InterfaceAssertion struct {
	// DepsField is the Deps field name the pair came from — used only for
	// a deterministic sort + a human-readable comment in the emitted file.
	DepsField string
	// Interface is the qualified interface type string the concrete type
	// satisfies (e.g. "user.Repository", "billing.PlanAssigner").
	Interface string
	// Concrete is the qualified concrete type string assigned to the
	// interface in wire_gen (e.g. "*db.PostgresRepository"). Always a
	// pointer or interface spelling — the matcher only emits assertions
	// for those (see AssignablePairs) so `(Concrete)(nil)` always compiles.
	Concrete string
	// Imports maps package path → package name for every package the two
	// type strings reference. The emitter dedupes across assertions.
	Imports map[string]string
}

InterfaceAssertion is one proven (concrete satisfies interface) pair, rendered ready for emission. Both type strings are qualified with the package selector form `<pkg>.<Name>` (via types.TypeString), and Imports lists every package path the two strings reference so the emitter can build a valid import block.

type InterfaceMethod

type InterfaceMethod struct {
	// Name is the method name as declared (e.g. "GetByID").
	Name string
	// Params is the rendered parameter list including names + types,
	// e.g. "ctx context.Context, id string". Empty when the method
	// takes no parameters.
	Params string
	// Results is the rendered result list with parens when there are
	// multiple, e.g. "(*db.User, error)". Empty when the method
	// returns nothing.
	Results string
	// ReturnStatement is the body of a stub implementation: either
	// "return <zeroes>" or empty when the method returns nothing.
	ReturnStatement string
}

InterfaceMethod is one method on a LocalInterface, in a shape directly consumable by the testing.go template's stub emitter.

type InventoryGenData

type InventoryGenData struct {
	Module      string
	RESTEnabled bool
	Services    []InventoryServiceData
	// ConnectImports are the *v1connect import lines needed for the
	// ConnectPath descriptor constants (and REST). Deduped + sorted.
	ConnectImports []string
}

InventoryGenData is the rendered template input for mounts_services.go.tmpl.

type InventoryGenInput

type InventoryGenInput struct {
	GenContext
	Services        []ServiceDef
	Packages        []BootstrapPackageData
	Workers         []BootstrapWorkerData
	Operators       []BootstrapOperatorData
	WebhookServices map[string]bool
}

InventoryGenInput carries everything GenerateInventory needs. Mirrors the bootstrap/inject inputs so naming stays in lockstep.

type InventoryServiceData

type InventoryServiceData struct {
	// Name is the runtime kebab name — DISPLAY + selection only.
	Name string
	// FieldName is the exported field on *Services holding the instance.
	FieldName string
	// Alias is the import alias for the service's handler package (for the
	// Deps-typed authorizer reference in the Mount closure).
	Alias string
	// ImportPath is the module-relative handler import path.
	ImportPath string
	// Package is the Go package clause.
	Package string
	// ConnectPkg / ProtoServiceName drive the ConnectPath descriptor and,
	// when REST is on, the connect import. Mirrors the bootstrap fields.
	ConnectPkg       string
	ProtoServiceName string
	// BaseService and Version carry the proto identity SPLIT into its
	// version-independent logical name and its proto API version (e.g.
	// proto package "billing.v1" -> BaseService "billing", Version "v1").
	// VERSION-AWARE SEAM (FORGE_SHAPE_REDESIGN — version-aware registry):
	// today identity fuses the version (the v1 rides in ConnectPath/ConnectPkg
	// and the import path), so a future `billing.v2` would register as a
	// SEPARATE service. Recording the version as EXPLICIT METADATA here — a
	// field, not an opaque part of identity — makes v2 an ADDITIVE change
	// later (a second Version on the same BaseService) rather than a breaking
	// redesign. It does NOT change today's behavior: ConnectPath, the mount
	// path, and the field keying are byte-identical for v1 projects; this is
	// pure additive metadata. Version is "" for an unversioned proto package.
	//
	// DEFERRED (NOT in this seam): per-version handler generation /
	// per-version mount adapters. When multi-version lands, the cmd layer
	// will group Inventory rows by BaseService and mount each Version's
	// ConnectPath on its own route; the Mount closure and a per-version
	// Services field are the extension points. Until then a project has at
	// most one Version per BaseService and the grouping is a no-op.
	BaseService string
	Version     string
	// HasWebhooks gates the webhook-route registration in the Mount body.
	HasWebhooks bool
	// HasAuthorizer is true when the service Deps declares an Authorizer —
	// the Mount closure threads its authz interceptor like services_gen.
	HasAuthorizer bool
}

InventoryServiceData is one service's rendered inventory row + Mount closure inputs.

type K3dListener

type K3dListener struct {
	GatewayName  string // for de-dup logs and comment lines
	ListenerName string
	Port         int
}

K3dListener is the minimal slice of a Gateway listener that the k3d-ports fragment needs. Callers project from internal/cli.GatewayEntity.Listeners → []K3dListener so this package stays free of the cli dependency.

type K3dPortsGenInput

type K3dPortsGenInput struct {
	GenContext

	Listeners []K3dListener // every dev-env listener, in any order
}

K3dPortsGenInput is the per-project input for GenerateK3dPorts. Embeds GenContext for ProjectDir + Checksums (ModulePath is unused — this fragment carries no Go imports).

type LocalInterface

type LocalInterface struct {
	// Name is the interface type name as declared (e.g. "Repository").
	Name string
	// Methods enumerates the interface's method set, with embedded
	// interfaces flattened so callers get a single list to walk.
	Methods []InterfaceMethod
}

LocalInterface describes one interface type declared in a handler (or package) directory. Used by the testing.go auto-stub generator to synthesize zero-value implementations for service-owned Deps fields whose type is locally declared.

We only consider interfaces declared in the same package as the service: cross-package interfaces would force the testing.go generator to chase imports across the project, and the interfaces that fail "Repo is required" today (Repository, CommandPublisher, AuditStore, etc.) are uniformly local to the handler that uses them. If a future need arises for cross-package stubs the parser can grow without changing the consumer's call sites.

type MCPGenInput

type MCPGenInput struct {
	GenContext

	ProjectName string       // emitted as the manifest's "project" field; "" tolerated
	Services    []ServiceDef // every parsed Connect service; empty → no-op
}

MCPGenInput is the per-project input for GenerateMCPManifest. The shape mirrors K3dPortsGenInput so the call-site in the generate pipeline stays uniform across codegen emitters. Embeds GenContext for ProjectDir + Checksums (ModulePath is unused — the manifest is JSON, not Go).

type MatchKind

type MatchKind int

MatchKind classifies the result of one Deps-field → AppExtras-field match attempt. The two matchers (bootstrap and wire_gen) treat the kinds identically — see the policy block in the file header.

const (
	// MatchNoName — AppExtras has no field with this Deps field's name.
	// Both matchers treat this as "no wire, no error".
	MatchNoName MatchKind = iota
	// MatchExactString — the pretty-printed type strings are byte-equal.
	// Legacy fast path: no go/types load required. Both matchers wire.
	MatchExactString
	// MatchAssignable — both packages loaded in one shared type universe
	// and AppExtras.<F> is assignable to Deps.<F>'s declared type per
	// go/types (narrow-interface case). Both matchers wire.
	MatchAssignable
	// MatchNameMismatch — name matches but the types are PROVEN not
	// assignable (both sides type-checked in a single universe).
	// Bootstrap drops the wire and relies on the post-generate lint to
	// surface the gap; wire_gen drops the app.<Field> resolution and
	// falls through to typed-zero + loud unresolved hint so the silent
	// compile-error class becomes loud.
	MatchNameMismatch
	// MatchUnavailable — assignability could not be proven either way
	// (go/types load failed, project not buildable mid-pipeline, field
	// invisible to the type checker). Per the deterministic fail-loud
	// policy (file header), BOTH consumers treat this as "wire the name
	// match": the compiler arbitrates a wrong wire loudly, whereas
	// emitting nil would silently un-wire a live collaborator.
	MatchUnavailable
	// MatchUnprovenBackstop — the Infra-field matcher (infra_assignability.go)
	// found NO assignable Infra field AND could not PROVE one is absent because
	// the universe is mid-write (some relevant field type did not type-check —
	// e.g. internal/app references the not-yet-regenerated Build, or the
	// new component package is a fresh stub). This is the generate-ORDERING
	// fragility class: raising a generate-time MissingProvider here is a
	// FALSE NEGATIVE (the user's Infra field, named differently from the Deps
	// field, would prove assignable on a clean load). The injector emits the
	// compile-time backstop `infra.<DepsField>` and lets the Go compiler
	// arbitrate — loud if genuinely missing, silently correct once the next
	// clean generate proves the assignable match. It is ONLY used by the
	// Infra matcher; the Deps/AppExtras matcher never returns it.
	MatchUnprovenBackstop
)

type MessageFieldDef

type MessageFieldDef struct {
	Name       string // proto field name: "page_size", "search", "active"
	ProtoType  string // "int32", "string", "bool"
	IsOptional bool   // true if the field has the "optional" label
	// MessageType carries the referenced message's name for message-typed
	// fields (e.g. "Item" for `Item item = 1;`, "google.protobuf.FieldMask"
	// for masks). ProtoType collapses every message field to the literal
	// "message" — which is how the CRUD shape matcher could never match an
	// update request's entity field against the entity name (the false
	// custom-read-shape stub — then spelled FORGE_CRUD_SHAPE_MISMATCH —
	// on forge's own scaffold). Additive:
	// `json:",omitempty"` keeps old descriptors parseable.
	MessageType string `json:",omitempty"`
}

MessageFieldDef represents a single field in a proto message definition.

type Method

type Method struct {
	Name            string
	InputType       string
	OutputType      string
	ClientStreaming bool
	ServerStreaming bool
	AuthRequired    bool // from (forge.v1.method).auth_required; defaults to true (fail-closed) when unannotated
	// RequiredRoles is the per-method role allow-list. NO proto annotation
	// populates it — forge.v1.MethodOptions has no required_roles field;
	// role policy is code (the user-owned handlers/<svc>/authorizer.go),
	// not proto. The field exists so the authorizer table shape can carry
	// roles if a non-proto source ever supplies them; today it is always
	// empty in parsed descriptors.
	RequiredRoles []string
	// AuthzCustom records (forge.v1.method).authz_custom = true — the method
	// delegates its authorization decision to a hand-written per-service
	// authorizer (a subject/identity/resource-scoped rule not expressible as a
	// role allow-list). Such a method carries NO RequiredRoles (the proto can't
	// express its rule), so a naive role-table emit would write it with EMPTY
	// roles — which on a role table reads as "any authenticated user allowed".
	// The flag lets the authorizer generator emit it FAIL-CLOSED instead, so the
	// generated table can never be misread as an any-authenticated grant. The
	// LIVE decision is enforced by the descriptor-driven RoleInterceptor +
	// the hand-written authorizer.go, not this table.
	AuthzCustom bool
	// Errors records the Connect/gRPC error codes this method may return,
	// derived from (forge.v1.method).errors. Values match connect.Code
	// names (e.g. "NotFound", "PermissionDenied"). Surfaced through
	// generated code so handler authors see the typed error contract at
	// a glance. Informational at runtime — no enforcement (yet).
	Errors []string
	// InputTypeFQ / OutputTypeFQ are the fully-qualified names of the
	// request/response messages (e.g. "shop.v1.CreateOrderRequest",
	// "google.protobuf.Empty"). They key into ServiceDef.Schemas for
	// deep JSON-Schema emission. Empty on descriptors produced by
	// older forge versions — consumers fall back to the short-name
	// InputType/OutputType + Messages map.
	InputTypeFQ  string `json:",omitempty"`
	OutputTypeFQ string `json:",omitempty"`
	// InputProtoFile / OutputProtoFile record the proto file path that
	// physically declares the input/output message. For RPCs whose
	// request/response live in the same proto file as the service these
	// equal ServiceDef.ProtoFile, but they differ when an RPC references
	// a message from another file (e.g. services/users/v1/users.proto's
	// ListUsers returns shared/v1/types.proto's Page). The frontend hooks
	// generator groups imports by these paths so each cross-file message
	// is imported from its declaring _pb.ts file rather than silently
	// referenced as an unresolved identifier.
	InputProtoFile  string
	OutputProtoFile string
}

Method represents a single RPC method.

func (Method) GoInputType

func (m Method) GoInputType() string

GoInputType returns the Go type reference for the input (handles Empty).

func (Method) GoOutputType

func (m Method) GoOutputType() string

GoOutputType returns the Go type reference for the output (handles Empty).

func (Method) IsInputEmpty

func (m Method) IsInputEmpty() bool

IsInputEmpty returns true if the input type is google.protobuf.Empty.

func (Method) IsOutputEmpty

func (m Method) IsOutputEmpty() bool

IsOutputEmpty returns true if the output type is google.protobuf.Empty.

type MethodTemplateData

type MethodTemplateData struct {
	Name            string // RPC method name, e.g. "GetItem"
	InputType       string // proto input message name, e.g. "GetItemRequest"
	OutputType      string // proto output message name, e.g. "GetItemResponse"
	ClientStreaming bool   // true if the client streams requests
	ServerStreaming bool   // true if the server streams responses
	AuthRequired    bool   // true if method_options.auth_required is set
}

MethodTemplateData holds per-method data for the embedded service templates.

type MissingHandlerResult

type MissingHandlerResult struct {
	NewMethods  []string // names of methods that were generated
	AllUpToDate bool     // true if no new methods were needed
}

MissingHandlerResult holds the result of scanning for missing handler stubs.

func GenerateMissingHandlerStubs

func GenerateMissingHandlerStubs(svc ServiceDef, projectDir, targetDir string, crudMethodNames map[string]bool, cs *checksums.FileChecksums) (*MissingHandlerResult, error)

GenerateMissingHandlerStubs scans the existing service directory for implemented methods on *Service, compares against the proto ServiceDef, and scaffolds stubs only for missing (non-CRUD, not-yet-implemented) methods directly into the USER-OWNED handlers.go — "scaffold and forget", not a forge-owned holding pen. If all methods are already implemented, it returns AllUpToDate=true.

Append semantics:

  • handlers.go absent: render the full handlers.go.tmpl for the missing methods and write it via writeUserScaffold (same as the initial scaffold).
  • handlers.go present: render a method-only fragment for the missing methods, append it to the file, then re-parse + ensure the required imports (context, fmt, connect, pb) are present and gofmt the whole file. The appended stubs land on *Service, so the next `forge generate` sees them as implemented (scanExistingMethods) and won't re-stub them.

If the user deletes handlers.go, forge re-scaffolding the missing stubs on the next run is acceptable/desired. There is no more handlers_gen.go — ever.

crudMethodNames optionally lists method names that CRUD gen will implement; stubs are skipped for these even if they don't exist yet in the package.

cs (the project checksum tracker) is retained for signature stability; the user-owned handlers.go is deliberately NOT checksum-tracked. The placeholder-replacement of handlers_scaffold_test.go likewise records no checksum: it becomes user-owned once the placeholder is filled in. The canonical handlers_test.go filename is reserved for the user. A nil cs is tolerated.

type MissingProvider

type MissingProvider struct {
	// Component is the consuming component's FieldName.
	Component string
	// Field is the Deps field name with no provider.
	Field string
	// Type is the declared field type with no provider.
	Type string
}

MissingProvider records a required Deps field that resolved to no producer and no PROVEN-assignable Infra field. GenerateInject returns an error built from these (see the file header's two-tier loudness).

type MockEntityTemplateData

type MockEntityTemplateData struct {
	EntityName       string       // "Patient" (PascalCase)
	EntityNamePlural string       // "Patients"
	EntitySlug       string       // "patients" (kebab-case for filename)
	SchemaImport     string       // "PatientSchema"
	TypeImport       string       // "Patient"
	ImportPath       string       // "services/clinic/v1/clinic_pb"
	Fields           []MockField  // fields to populate in mock records
	Records          []MockRecord // 10 mock records
}

MockEntityTemplateData holds data for rendering a single entity's TypeScript mock data file (e.g., frontends/<fe>/src/mocks/patients.ts).

func EntityDefToMockData

func EntityDefToMockData(entity EntityDef, svc ServiceDef) MockEntityTemplateData

EntityDefToMockData converts an EntityDef (parsed from proto) and its associated ServiceDef into MockEntityTemplateData for template rendering. It generates the same deterministic mock values as seed_gen.go.

type MockField

type MockField struct {
	Name      string // camelCase TS field name: "orgId"
	ProtoName string // snake_case proto field name: "org_id"
	TSType    string // "string", "number", "boolean"
}

MockField describes a single field in the proto message for mock data.

type MockFieldValue

type MockFieldValue struct {
	Name  string // camelCase field name
	Value string // TypeScript literal: `"abc"`, `42`, `true`
	Last  bool   // true if this is the last field (for comma handling in templates)
}

MockFieldValue is a field name + its literal TypeScript value.

type MockInspector

type MockInspector struct {
	contractkit.Recorder
	DetectFallibleConstructorFunc func(string) (bool, error)
	DetectDepsDBFieldFunc         func(string) (bool, error)
}

MockInspector is a test mock for the Inspector interface.

The embedded contractkit.Recorder records every call so tests can assert call counts and captured arguments. Set XxxFunc fields to override per-method behaviour; unset methods return the canonical "MockInspector.<Method>Func not set" error.

func (*MockInspector) DetectDepsDBField

func (m *MockInspector) DetectDepsDBField(dir string) (bool, error)

func (*MockInspector) DetectFallibleConstructor

func (m *MockInspector) DetectFallibleConstructor(dir string) (bool, error)

type MockParser

type MockParser struct {
	contractkit.Recorder
	ParseServicesFromProtosFunc  func(string, string) ([]ServiceDef, error)
	ParseEntityProtosFunc        func(string) ([]EntityDef, error)
	ParseConfigProtoFunc         func(string) ([]ConfigMessage, error)
	ParseConfigProtosFromDirFunc func(string) ([]ConfigMessage, error)
	GetModulePathFunc            func(string) (string, error)
}

MockParser is a test mock for the Parser interface.

The embedded contractkit.Recorder records every call so tests can assert call counts and captured arguments. Set XxxFunc fields to override per-method behaviour; unset methods return the canonical "MockParser.<Method>Func not set" error.

func (*MockParser) GetModulePath

func (m *MockParser) GetModulePath(dir string) (string, error)

func (*MockParser) ParseConfigProto

func (m *MockParser) ParseConfigProto(protoPath string) ([]ConfigMessage, error)

func (*MockParser) ParseConfigProtosFromDir

func (m *MockParser) ParseConfigProtosFromDir(dir string) ([]ConfigMessage, error)

func (*MockParser) ParseEntityProtos

func (m *MockParser) ParseEntityProtos(projectDir string) ([]EntityDef, error)

func (*MockParser) ParseServicesFromProtos

func (m *MockParser) ParseServicesFromProtos(dir string, projectDir string) ([]ServiceDef, error)

type MockRecord

type MockRecord struct {
	Fields []MockFieldValue
}

MockRecord is a single mock object with field values.

type MockService

type MockService struct {
	contractkit.Recorder
	GenerateServiceStubFunc         func(ServiceDef, string, ...map[string]bool) error
	RegenerateServiceFileFunc       func(ServiceDef, string) error
	GenerateMissingHandlerStubsFunc func(ServiceDef, string, string, map[string]bool, *checksums.FileChecksums) (*MissingHandlerResult, error)
	GenerateMockFunc                func(ServiceDef, string) (bool, error)
	GenerateAuthorizerFunc          func([]ServiceDef, string, string, map[string]bool, *checksums.FileChecksums) error
	GenerateAuthMiddlewareFunc      func(*config.AuthConfig, string, []string, string, *checksums.FileChecksums) error
	GenerateTenantMiddlewareFunc    func(*config.MultiTenantConfig, string, *checksums.FileChecksums) error
	GenerateCRUDHandlersFunc        func(ServiceDef, []CRUDMethod, string, string, *checksums.FileChecksums) error
	GenerateCRUDTestsFunc           func(ServiceDef, []CRUDMethod, string, string, *checksums.FileChecksums) error
	GenerateCmdServerFunc           func([]ConfigMessage, string, *checksums.FileChecksums) error
	GenerateCmdServerWithFieldsFunc func(map[string]bool, string, string, *checksums.FileChecksums) error
	GenerateConfigLoaderFunc        func([]ConfigMessage, string, *checksums.FileChecksums) error
	GenerateBootstrapTestingFunc    func(BootstrapTestingGenInput) error
	GenerateMigrateFunc             func(string, string, bool, *checksums.FileChecksums) error
}

MockService is a test mock for the Service interface.

The embedded contractkit.Recorder records every call so tests can assert call counts and captured arguments. Set XxxFunc fields to override per-method behaviour; unset methods return the canonical "MockService.<Method>Func not set" error.

func (*MockService) GenerateAuthMiddleware

func (m *MockService) GenerateAuthMiddleware(cfg *config.AuthConfig, modulePath string, skipMethods []string, targetDir string, cs *checksums.FileChecksums) error

func (*MockService) GenerateAuthorizer

func (m *MockService) GenerateAuthorizer(services []ServiceDef, modulePath string, targetDir string, skipDirs map[string]bool, cs *checksums.FileChecksums) error

func (*MockService) GenerateBootstrapTesting

func (m *MockService) GenerateBootstrapTesting(in BootstrapTestingGenInput) error

func (*MockService) GenerateCRUDHandlers

func (m *MockService) GenerateCRUDHandlers(svc ServiceDef, crudMethods []CRUDMethod, modulePath string, projectDir string, cs *checksums.FileChecksums) error

func (*MockService) GenerateCRUDTests

func (m *MockService) GenerateCRUDTests(svc ServiceDef, crudMethods []CRUDMethod, modulePath string, projectDir string, cs *checksums.FileChecksums) error

func (*MockService) GenerateCmdServer

func (m *MockService) GenerateCmdServer(messages []ConfigMessage, targetDir string, cs *checksums.FileChecksums) error

func (*MockService) GenerateCmdServerWithFields

func (m *MockService) GenerateCmdServerWithFields(configFields map[string]bool, authProvider string, targetDir string, cs *checksums.FileChecksums) error

func (*MockService) GenerateConfigLoader

func (m *MockService) GenerateConfigLoader(messages []ConfigMessage, targetDir string, cs *checksums.FileChecksums) error

func (*MockService) GenerateMigrate

func (m *MockService) GenerateMigrate(targetDir string, modulePath string, hasMigrations bool, cs *checksums.FileChecksums) error

func (*MockService) GenerateMissingHandlerStubs

func (m *MockService) GenerateMissingHandlerStubs(svc ServiceDef, projectDir string, targetDir string, crudMethodNames map[string]bool, cs *checksums.FileChecksums) (*MissingHandlerResult, error)

func (*MockService) GenerateMock

func (m *MockService) GenerateMock(svc ServiceDef, mockDir string) (bool, error)

func (*MockService) GenerateServiceStub

func (m *MockService) GenerateServiceStub(svc ServiceDef, targetDir string, crudMethodNames ...map[string]bool) error

func (*MockService) GenerateTenantMiddleware

func (m *MockService) GenerateTenantMiddleware(mt *config.MultiTenantConfig, targetDir string, cs *checksums.FileChecksums) error

func (*MockService) RegenerateServiceFile

func (m *MockService) RegenerateServiceFile(svc ServiceDef, targetDir string) error

type MockTransportEntity

type MockTransportEntity struct {
	EntityName       string // "Patient"
	EntityNamePlural string // "Patients"
	EntitySlug       string // "patients" (for mock data import path)
	ServiceName      string // "ClinicService" (short, used in display only)
	// ServiceTypeName is the FULLY-QUALIFIED proto service name, e.g.
	// "demo.v1.ClinicService". Connect v2's runtime
	// `method.parent.typeName` returns this form, so the mock transport
	// must build case keys from it (`${ServiceTypeName}/${RPC}`) or the
	// fall-through dispatch silently never matches.
	ServiceTypeName string // "demo.v1.ClinicService"
	ListRPC         string // "ListPatients"
	GetRPC          string // "GetPatient"
	CreateRPC       string // "CreatePatient"
	UpdateRPC       string // "UpdatePatient"
	DeleteRPC       string // "DeletePatient"
	HasList         bool
	HasGet          bool
	HasCreate       bool
	HasUpdate       bool
	HasDelete       bool
	// ItemsField is the camelCase (protojson) name of the list response's
	// repeated field — the key the mock List handler must set on the
	// ListXxxResponse it builds. It mirrors PageTemplateData.ItemsField:
	// the ACTUAL repeated proto field name (e.g. `keys`), not the
	// camelCased entity plural. The mock store variable keeps the plural
	// camelCase identifier; only the response-message KEY uses this.
	ItemsField string
	// PkFieldCamel is the camelCase name of the entity message's
	// PRIMARY-KEY field ("id", "usageEventId", ...). The mutable session
	// store keys records by this field; hardcoding "id" breaks for entities
	// whose PK column isn't literally `id` (e.g. a `usage_event_id` PK
	// projects to a message with no `id` field, failing `tsc`).
	PkFieldCamel string
	// GetEntityFieldCamel / CreateEntityFieldCamel are the camelCase names
	// of the field that wraps the entity on the Get / Create+Update RESPONSE
	// messages. The proto is free to name this field anything
	// (`GetLLMKeyResponse { LLMKey key = 1; }` → `key`, not `lLMKey`), so the
	// mock dispatch must read it off the response descriptor instead of
	// assuming `camelCase(EntityName)` — a wrong key fails `tsc` with "object
	// literal may only specify known properties".
	GetEntityFieldCamel    string
	CreateEntityFieldCamel string
	ImportPath             string // service proto import path for response-schema imports
	// EntityImportPath is the module declaring the ENTITY message schema
	// ("db/v1/patients_pb"). May differ from ImportPath when the entity
	// lives in its own proto file; the mutable-store Create/Update paths
	// need the entity schema to build new records.
	EntityImportPath string
	TypeImport       string // "Patient"
	SchemaImport     string // "PatientSchema"
	// Response/request type names
	ListResponseType   string
	GetResponseType    string
	CreateRequestType  string
	CreateResponseType string
	UpdateRequestType  string
	GetRequestType     string
	DeleteRequestType  string
}

MockTransportEntity represents one entity in the mock transport routing.

func ExtractMockTransportEntities

func ExtractMockTransportEntities(services []ServiceDef, entities []EntityDef) []MockTransportEntity

ExtractMockTransportEntities builds MockTransportEntity data from services and entity definitions. It pairs CRUD page data with entity info.

type MockTransportSchemaImportGroup

type MockTransportSchemaImportGroup struct {
	ImportPath string   // proto module path, e.g. "services/api/v1/api_pb"
	Symbols    []string // schema symbols imported from this module, dedup'd + sorted
}

MockTransportSchemaImportGroup bundles every response-schema symbol the mock-transport.ts file imports from a single proto-generated module. Two entities whose schemas live in the same `@/gen/services/api/v1/api_pb` module merge into one import statement; entities pointing at distinct modules each get their own group.

func BuildMockTransportSchemaImportGroups

func BuildMockTransportSchemaImportGroups(entities []MockTransportEntity) []MockTransportSchemaImportGroup

BuildMockTransportSchemaImportGroups groups response-schema imports by the entity's proto module path. Each entity contributes the same per-RPC schema set the per-entity template loop used to emit (`{ListResponseType,GetResponseType,CreateResponseType}Schema` gated on `HasList`/`HasGet`/`HasCreate||HasUpdate`). Duplicate symbols within a group are collapsed; the order is sorted for deterministic output across runs.

type MockTransportTemplateData

type MockTransportTemplateData struct {
	Entities []MockTransportEntity
	// SchemaImportGroups carries the per-ImportPath aggregation of
	// response-schema imports the mock-transport template needs. The
	// template iterates these to emit ONE merged `import { ... } from
	// "@/gen/<path>"` statement per source module, instead of one per
	// entity. Pre-aggregating in Go (vs. with a groupBy template helper)
	// keeps the template loop trivially auditable and lets us preserve
	// entity ordering inside each group.
	SchemaImportGroups []MockTransportSchemaImportGroup
}

MockTransportTemplateData holds data for rendering the mock-transport.ts file.

func (MockTransportTemplateData) HasWritableEntities

func (d MockTransportTemplateData) HasWritableEntities() bool

HasWritableEntities reports whether any entity has a Create or Update RPC — gates the MessageInitShape type import in the transport template (used only by the mutable-store write paths; an unconditional import would trip no-unused-vars on read-only projects).

type OperatorSpec

type OperatorSpec struct {
	Name string
	Path string
}

OperatorSpec is the operator-side analog of WorkerSpec. See WorkerSpec for the path-honoring rationale.

type PageField

type PageField struct {
	Name  string // "title" (camelCase)
	Label string // "Title" (display name)
	Type  string // "text", "number", "checkbox", "date", "textarea"
	// ProtoName is the original snake_case proto field name ("created_at")
	// — the AIP-134 update_mask path for this field.
	ProtoName string
	Required  bool
	ProtoType string // original proto type for reference
	// IsBigInt marks 64-bit integer fields — protobuf-es types them as
	// bigint, so form submissions convert the zod number before mutate().
	IsBigInt bool
	// IsRepeated marks repeated scalar fields (descriptor ProtoType
	// "[]string" etc.). The form renders a comma-separated text input and
	// the submit handler splits it back into the array the RPC expects —
	// without the split, the generated page assigned a string to a
	// string[] request field and failed the TypeScript build.
	IsRepeated bool
	// RepeatedNumeric marks repeated numeric fields whose elements need
	// Number() conversion after the comma split.
	RepeatedNumeric bool
}

PageField represents a form field derived from a proto message field.

type PageTemplateData

type PageTemplateData struct {
	EntityName       string // "Task" (PascalCase)
	EntityNamePlural string // "Tasks"
	EntitySlug       string // "tasks" (kebab-case for URL)
	ServiceName      string // "TaskService"
	ServiceNameCamel string // "taskService"
	HooksImportPath  string // "@/hooks/task-service-hooks"
	TypesImportPath  string // "@/gen/services/tasks/v1/tasks_pb"
	ListRPC          string // "ListTasks" (PascalCase, matching hook name)
	GetRPC           string // "GetTask"
	CreateRPC        string // "CreateTask"
	UpdateRPC        string // "UpdateTask"
	DeleteRPC        string // "DeleteTask"
	HasList          bool
	HasGet           bool
	HasCreate        bool
	HasUpdate        bool
	HasDelete        bool
	// ItemsField is the camelCase (protojson) accessor for the list
	// response's repeated field — the array the list hook's `data` holds.
	// It is the ACTUAL repeated proto field name on the ListXxxResponse
	// message (e.g. `keys` for `ListLLMKeysResponse { repeated LLMKey keys
	// = 1; }`), NOT the camelCased entity plural. They usually coincide
	// ("tasks" for ListTasksResponse.tasks) but diverge whenever the proto
	// names the field differently — and a wrong accessor silently yields
	// `undefined`, breaking the list page, the dashboard count tile, and
	// the mock transport's response shape all at once. Falls back to the
	// camelCased plural for older descriptors that don't carry the field.
	ItemsField   string
	CreateFields []PageField // Fields for the create form
	UpdateFields []PageField // Fields for the edit form
	// UpdateEntityFieldCamel is the camelCase request field wrapping the
	// entity when the update request follows AIP-134 ("task" for
	// `Task task = 1;`). The edit page then nests the form values under
	// it (with the PK inside) instead of spreading them at the top level.
	// Empty for flat update requests (legacy id+fields shape).
	UpdateEntityFieldCamel string
	// UpdateMaskFieldCamel is the camelCase google.protobuf.FieldMask
	// field on the update request ("updateMask"); the edit page sends a
	// mask naming exactly the form's fields so the server's masked write
	// can't clobber columns the form never edits. Empty when the request
	// has no mask.
	UpdateMaskFieldCamel string
	// Response type names for imports
	ListResponseType   string // "ListTasksResponse"
	GetResponseType    string // "GetTaskResponse"
	CreateRequestType  string // "CreateTaskRequest"
	CreateResponseType string // "CreateTaskResponse"
	UpdateRequestType  string // "UpdateTaskRequest"
	GetRequestType     string // "GetTaskRequest"
	DeleteRequestType  string // "DeleteTaskRequest"

	// EntityTypeImportPath is the TS module declaring the entity type
	// ("@/gen/db/v1/tasks_pb"). May differ from TypesImportPath when the
	// entity message lives in its own proto file.
	EntityTypeImportPath string
	// Columns drives the list page's typed column array and the detail
	// page's field rows. Only renderable kinds are included (scalars,
	// enums, timestamps, repeated scalars) — nested messages and maps
	// don't belong in a table cell.
	Columns []EntityPageField
	// SearchFields are the camelCase string-typed fields client-side
	// search filters over. Empty → the list page omits the search box.
	SearchFields []string
	// DisplayField is the camelCase string field used as the human title
	// ("name", then "title"); empty when the entity has neither.
	DisplayField string
	// PkFieldCamel is the camelCase primary-key field ("id").
	PkFieldCamel string
	// HasBadgeColumns reports whether any column renders as a Badge —
	// gates the Badge / enumBadgeVariant imports in page templates.
	HasBadgeColumns bool
	// HasDateCreateFields / HasDateUpdateFields gate the timestamp
	// conversion imports (timestampFromDate / toDatetimeLocal) in the
	// create and edit form templates.
	HasDateCreateFields bool
	HasDateUpdateFields bool
}

PageTemplateData holds data for rendering a single entity's CRUD pages.

func ExtractCRUDEntities

func ExtractCRUDEntities(svc ServiceDef) []PageTemplateData

ExtractCRUDEntities analyzes a service's methods and returns PageTemplateData for each entity that has CRUD-pattern RPCs.

type Parser

type Parser interface {
	ParseServicesFromProtos(dir string, projectDir string) ([]ServiceDef, error)
	ParseEntityProtos(projectDir string) ([]EntityDef, error)
	ParseConfigProto(protoPath string) ([]ConfigMessage, error)
	ParseConfigProtosFromDir(dir string) ([]ConfigMessage, error)
	GetModulePath(dir string) (string, error)
}

Parser reads forge_descriptor.json + go.mod to produce ServiceDefs, EntityDefs, ConfigMessages, and the user project's module path. No files are written.

func NewParser

func NewParser(_ Deps) Parser

NewParser constructs the descriptor / go.mod parser surface.

type ResolvedComponent

type ResolvedComponent struct {
	// Dir is the component's source directory (projectDir/roleRoot/ImportLeaf).
	// Always populated — when FromDisk is false it points at the directory
	// forge WOULD create for a fresh scaffold of this name.
	Dir string

	// ImportLeaf is the directory path relative to the role root, in
	// forward-slash form (e.g. "engine_shadow", "mcp/database"). This is
	// the segment generated import lines must use — it reflects what is
	// actually on disk, not what the naming rules would synthesize.
	ImportLeaf string

	// PackageName is the Go package name. When FromDisk is true it is the
	// package clause parsed from the directory's .go files (which may
	// legally differ from the directory name, e.g. workers/engine_shadow
	// declaring `package engineshadow`). When FromDisk is false it is the
	// synthesized scaffold name.
	PackageName string

	// FromDisk reports whether the directory was found on disk. False
	// means the caller is looking at a component that hasn't been
	// scaffolded yet and PackageName/ImportLeaf are synthesized.
	FromDisk bool
}

ResolvedComponent is the disk-first identity of one component.

func ResolveComponentDir

func ResolveComponentDir(projectDir, roleRoot, name string) (ResolvedComponent, error)

ResolveComponentDir locates the existing directory for a component (role root "handlers", "workers", "operators", or "internal") and returns its disk-first identity: the real directory leaf for import lines plus the real package clause for selectors/aliases.

When no candidate directory exists, the synthesized scaffold identity is returned with FromDisk=false and a nil error — that's the legitimate "forge is about to create this component" path. An empty projectDir always takes the synthesized path (callers like unit tests pass "" to mean "no project context").

When a candidate directory exists but its package clause can't be determined (no parseable .go file, or conflicting clauses), the error from ParsePackageClause is returned verbatim — see that function for the diagnostic shape. Callers must NOT swallow this error and fall back to synthesis: emitting a guessed import/selector for a directory that demonstrably exists is the silent-corruption mode this resolver exists to kill.

func ResolveServiceComponent

func ResolveServiceComponent(projectDir, svcName string) (ResolvedComponent, error)

ResolveServiceComponent is ResolveComponentDir specialized for services: it strips the proto "Service" suffix (callers hold either the proto name "EngineShadowService" or the forge.yaml name "engine-shadow"; both must resolve to the same handlers/<x> dir) and probes under handlers/.

type ScenarioRpcData

type ScenarioRpcData struct {
	Entries     []ScenarioRpcEntry
	TypeImports []HookImportGroup // type-only imports, grouped per declaring module
}

ScenarioRpcData drives scenario-rpcs.ts.tmpl: a typed handler map keyed by `${serviceTypeName}/${methodName}` whose values take the TYPED request and must return a MessageInitShape of the response schema. This is what kills the snake_case-payload silent failure: a scenario returning `{ user_name: "x" }` for a `userName` field fails tsc instead of rendering empty cells.

func BuildScenarioRpcData

func BuildScenarioRpcData(services []ServiceDef) ScenarioRpcData

BuildScenarioRpcData collects every unary RPC across all services. Streaming RPCs are reachable through the map's string index signature — there is no canonical typed return shape for an arbitrary stream.

type ScenarioRpcEntry

type ScenarioRpcEntry struct {
	Key            string // "demo.v1.TaskService/GetTask" — matches method.parent.typeName dispatch
	RequestType    string // "GetTaskRequest"
	ResponseSchema string // "GetTaskResponseSchema"
}

ScenarioRpcEntry is one unary RPC row in the generated typed scenario handler map (src/mocks/scenario-rpcs_gen.ts).

type SchemaFieldDef

type SchemaFieldDef struct {
	Name string `json:"name"` // proto field name, snake_case ("page_size")
	// Kind is the proto scalar kind name ("string", "int32", "bool",
	// "bytes", ...) or one of the structured markers "message", "enum",
	// "map".
	Kind string `json:"kind"`
	// TypeName is the fully-qualified message/enum name when Kind is
	// "message" or "enum" (e.g. "shop.v1.Address",
	// "google.protobuf.Timestamp"). Empty for scalars and maps.
	TypeName string `json:"type_name,omitempty"`
	// Repeated marks proto `repeated` fields (JSON arrays). Always
	// false for maps — proto maps are repeated entry messages under
	// the hood, but their JSON encoding is an object, not an array.
	Repeated bool `json:"repeated,omitempty"`
	// Optional is true when the field carries the explicit `optional`
	// label (proto3 field presence). Optional fields stay out of JSON
	// Schema `required` lists.
	Optional bool `json:"optional,omitempty"`
	// Oneof is the containing oneof group's name for members of a
	// real (non-synthetic) oneof. proto3 `optional` fields use a
	// synthetic oneof internally; those report "" here and Optional
	// true instead.
	Oneof string `json:"oneof,omitempty"`
	// Map-typed fields (Kind == "map") record the key/value kinds.
	// MapValueTypeName names the fully-qualified message/enum when the
	// value kind is "message"/"enum".
	MapKeyKind       string `json:"map_key_kind,omitempty"`
	MapValueKind     string `json:"map_value_kind,omitempty"`
	MapValueTypeName string `json:"map_value_type_name,omitempty"`
}

SchemaFieldDef is the deep-schema sibling of MessageFieldDef: one field of a message in ServiceDef.Schemas, carrying enough type information to project a full (nested) JSON Schema without consulting the proto source. Unlike MessageFieldDef.ProtoType (which collapses messages/enums/maps into opaque strings), this keeps the type graph: message and enum fields name their fully-qualified target so a schema emitter can $ref into a shared definitions block.

type Service

type Service interface {
	// Service-level handler scaffolds.
	GenerateServiceStub(svc ServiceDef, targetDir string, crudMethodNames ...map[string]bool) error
	RegenerateServiceFile(svc ServiceDef, targetDir string) error
	GenerateMissingHandlerStubs(svc ServiceDef, projectDir, targetDir string, crudMethodNames map[string]bool, cs *checksums.FileChecksums) (*MissingHandlerResult, error)
	GenerateMock(svc ServiceDef, mockDir string) (bool, error)

	// Authorization / auth middleware / tenant middleware.
	GenerateAuthorizer(services []ServiceDef, modulePath string, targetDir string, skipDirs map[string]bool, cs *checksums.FileChecksums) error
	GenerateAuthMiddleware(cfg *config.AuthConfig, modulePath string, skipMethods []string, targetDir string, cs *checksums.FileChecksums) error
	GenerateTenantMiddleware(mt *config.MultiTenantConfig, targetDir string, cs *checksums.FileChecksums) error

	// CRUD generation.
	GenerateCRUDHandlers(svc ServiceDef, crudMethods []CRUDMethod, modulePath string, projectDir string, cs *checksums.FileChecksums) error
	GenerateCRUDTests(svc ServiceDef, crudMethods []CRUDMethod, modulePath string, projectDir string, cs *checksums.FileChecksums) error

	// Config loader / cmd-server wiring.
	GenerateCmdServer(messages []ConfigMessage, targetDir string, cs *checksums.FileChecksums) error
	GenerateCmdServerWithFields(configFields map[string]bool, authProvider string, targetDir string, cs *checksums.FileChecksums) error
	GenerateConfigLoader(messages []ConfigMessage, targetDir string, cs *checksums.FileChecksums) error

	// pkg/app bootstrap files.
	GenerateBootstrapTesting(in BootstrapTestingGenInput) error
	GenerateMigrate(targetDir string, modulePath string, hasMigrations bool, cs *checksums.FileChecksums) error
}

Service is the file-emission surface of the codegen package.

Every method writes one or more files into the user-project working directory. Methods are grouped by file produced so consumers (the generator orchestrator, tests) can stub a focused subset.

func New

func New(_ Deps) Service

New constructs the file-emission Service.

type ServiceDef

type ServiceDef struct {
	Name       string // "EchoService"
	Package    string // "echo.v1"
	GoPackage  string // "github.com/.../gen/proto/echo/v1"
	PkgName    string // "echov1"
	Methods    []Method
	ProtoFile  string
	ModulePath string                       // e.g., "github.com/demo-project"
	Messages   map[string][]MessageFieldDef // message name → fields (e.g., "ListPatientsRequest" → [...])

	// Schemas is the deep type graph for full JSON-Schema emission
	// (MCP manifest): fully-qualified message name → fields, covering
	// every message transitively reachable from any method's input or
	// output type. Well-known types (google.protobuf.*) are NOT
	// included — consumers map those to fixed JSON encodings matching
	// protojson. Empty/nil on descriptors produced by forge versions
	// before this field existed; consumers must fall back to the
	// shallow Messages map in that case. Keyed by fully-qualified name
	// (e.g. "shop.v1.Address") so cross-package short-name collisions
	// can't alias two different messages.
	Schemas map[string][]SchemaFieldDef `json:",omitempty"`

	// SchemaFiles maps a fully-qualified message name (the same keys used
	// in Schemas) to the proto file that physically DECLARES that message.
	// For a message declared in the service's own proto file this equals
	// ProtoFile; for a message pulled in from another file (e.g. a shared
	// `shared.proto` holding the domain entity messages while the CRUD
	// service lives in `services/<svc>/v1/<svc>.proto`) it differs. Codegen
	// that emits a `*Schema` / type import for a message must resolve the
	// import path from the message's DEFINING file, not the service file —
	// otherwise it imports a symbol from a `_pb.ts` module that doesn't
	// export it. Empty/nil on descriptors produced by forge versions before
	// this field existed; consumers must fall back to the service ProtoFile.
	SchemaFiles map[string]string `json:",omitempty"`

	// Enums maps fully-qualified enum name → declared value names, in
	// proto declaration order, for every enum reachable through
	// Schemas. protojson encodes enums as their value-name strings, so
	// this is exactly the "enum" list a JSON Schema needs.
	Enums map[string][]string `json:",omitempty"`
}

ServiceDef represents a parsed Connect RPC service definition.

func ParseServicesFromProtos

func ParseServicesFromProtos(dir string, projectDir string) ([]ServiceDef, error)

ParseServicesFromProtos reads service definitions from the forge descriptor. Falls back to empty if the descriptor does not exist yet.

type ServiceKeyResolver

type ServiceKeyResolver struct {
	// contains filtered or unexported fields
}

ServiceKeyResolver is the default TypeResolver: it resolves a Deps field type against the set of component-exposed service type keys (each producer's ServiceTypeKey, e.g. "user.Service"). The match is on the pretty-printed type string the Deps parser already produces, tolerating a leading pointer (`*user.Service`) since a consumer may hold either the interface value or a pointer to it.

This is intentionally string-structural rather than go/types-based: it is pure, deterministic, and cheap, and the package-qualified Service interface name is unambiguous across a project (one `Service` per component package by the strict-contract-names convention). When a project genuinely needs assignability-by-implementation (a narrow collaborator interface satisfied by another component's Service), that is surfaced as an unresolved dep + TODO in build.go rather than guessed — matching the fail-loud stance of the wire matcher.

func NewServiceKeyResolver

func NewServiceKeyResolver(comps []BuildComponent) *ServiceKeyResolver

NewServiceKeyResolver indexes comps by their import-path key (primary) and their package-clause key (ambiguity-tracked fallback). Components with an empty ServiceTypeKey produce nothing and are skipped.

func (*ServiceKeyResolver) Resolve

func (r *ServiceKeyResolver) Resolve(consumer BuildComponent, depsType string) string

Resolve matches a Deps field type to a producing component FieldName, disambiguating the field's package-clause prefix through the consumer's import block to a full import path (import-path identity). Falls back to the bare clause only when it is unambiguous and the import block didn't resolve it.

type ServiceTemplateData

type ServiceTemplateData struct {
	ServiceName    string // e.g. "EchoService" (or hyphenated CLI form)
	ServicePackage string // Go package CLAUSE, e.g. "echo" (disk-resolved for existing dirs)
	// ServiceImportPath is the internal/handlers/ directory leaf used in scaffolded
	// test imports (`{{.Module}}/internal/handlers/{{.ServiceImportPath}}`). Equals
	// ServicePackage for fresh scaffolds; for EXISTING dirs it is the real
	// directory name, which may legally differ from the package clause.
	ServiceImportPath   string
	Module              string               // e.g. "github.com/demo-project"
	ProtoImportPath     string               // e.g. "proto/services/echo" (without /v1)
	ProtoPackage        string               // same as ProtoImportPath for handlers.go.tmpl
	ProtoConnectPackage string               // e.g. "echov1connect"
	HandlerName         string               // e.g. "EchoService"
	ProtoFileSymbol     string               // e.g. "File_services_echo_v1_echo_proto"
	Methods             []MethodTemplateData // method data for handlers.go.tmpl and test templates
	// TestHelperName is the disambiguated suffix that the bootstrap testing
	// generator uses for `app.NewTest<X>` and `app.NewTest<X>Server` helpers.
	// Equal to PascalCase(ServicePackage) when there's no cross-role
	// collision, else "Svc" + PascalCase(ServicePackage) — matching
	// AssignBootstrapAliases / GenerateBootstrapTesting's collision rule.
	// Test scaffold templates reference this rather than re-pascal-casing
	// ServiceName so the call site stays in sync with the actual factory.
	TestHelperName string
}

ServiceTemplateData holds the data shape expected by the embedded service templates.

type TenantTemplateData

type TenantTemplateData struct {
	ClaimField string // JWT claim to extract tenant ID from (e.g. "org_id")
	ColumnName string // DB column name for tenant scoping (e.g. "org_id")
}

TenantTemplateData holds the data shape expected by the tenant middleware template.

type TypeResolver

type TypeResolver interface {
	// Resolve returns the producing component's FieldName for a Deps
	// field of the given declared type, or "" if none. consumer is the
	// component that DECLARED the field — its import block disambiguates
	// the field's package-clause prefix to a full import path, so two
	// producers sharing a package clause resolve to the correct distinct
	// producer (import-path identity, not bare clause).
	Resolve(consumer BuildComponent, depsType string) string
}

TypeResolver maps a Deps field's declared type to the FieldName of the component that PRODUCES that type, or "" when no registered component produces it (the field is a conventional dep filled from Infra, or an external collaborator). Implementations resolve by TYPE — see depsTypeResolver for the production matcher and the test helpers for in-memory variants.

type UnresolvedAutoStub

type UnresolvedAutoStub struct {
	// FieldName is the Deps field name as declared.
	FieldName string
	// TypeExpr is the unresolved type expression as written in the
	// Deps struct (e.g. "external.Client").
	TypeExpr string
}

UnresolvedAutoStub is one Deps field whose cross-package selector type couldn't be turned into a synthesized stub. Surfaces in the generated testing.go as a TODO comment so the user knows to hand-roll an override.

type UnresolvedPlaceholder

type UnresolvedPlaceholder struct {
	// FieldName is the AppExtras field name.
	FieldName string

	// CurrentType is the type as declared today (typically "any").
	CurrentType string

	// TargetType is the type the user promised to tighten to.
	TargetType string
}

UnresolvedPlaceholder is an AppExtras field that carries the `forge:placeholder` marker but is still typed `any`. The build-time gate (forge lint --wire-coverage) treats these as ERRORS — a field the user promised to tighten to a real type but hasn't yet.

Retained after the old name-matched wire_gen unit was retired (FORGE_SHAPE_REDESIGN §2): the placeholder lint still reads pkg/app/app_extras.go and reports markers left typed `any`.

type WorkerSpec

type WorkerSpec struct {
	Name string // user-facing name from forge.yaml (e.g. "climatology_refresh")
	Path string // optional dir path from forge.yaml (e.g. "workers/climatology_refresh"); empty falls back to naming.GoPackage(name)
}

WorkerSpec carries a worker's user-facing name plus the optional `path:` field declared in forge.yaml. When Path is non-empty, the dir leaf (`filepath.Base(Path)`) becomes the import-path segment and the Go-package alias — preserving the user's exact dir name. When Path is empty, behavior falls back to `naming.GoPackage(name)` which produces the snake_case canonical form (`calibrator_refit` stays `calibrator_refit`, `email-sender` becomes `email_sender`).

OperatorSpec mirrors this for operators (same path-honoring rule).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL