Help us shape the future of Go! Take the 2026 Go Developer Survey

codegen

package
v0.1.46 Latest Latest
Warning

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

Go to latest
Published: Oct 20, 2026 License: Apache-2.0 Imports: 56 Imported by: 0

Documentation

Overview

Package codegen — config_native_emit.go wires the KCL config emitters (config_schema_gen.go, config_projection_gen.go, and the per-env config.k scaffolder below) into the generate pipeline.

Two ownership tiers, mirroring the design:

  • config_gen.k is PROJECT-LEVEL and Tier-1 forge-owned (writeForgeOwned): ONE file per project, regenerated from proto on every `forge generate`. It carries the config TYPE and the projection BEHAVIOR together — turning a typed AppConfig into the agnostic-core env map every workload's env is built from. (It was two files, config_schema.k + config_projection.k, until both halves — always generated together, always consistent — were merged into one Tier-1 file with one hash guard. See GenerateConfigKCL.)
  • deploy/kcl/<env>/config.k is PER-ENV and USER-OWNED (write-if-absent): a typed AppConfig instance carrying the per-env values. forge scaffolds it once from the proto's own defaults and never clobbers later edits.

Package codegen — config_projection_gen.go emits the VALUE-projection half, and assembles it with the TYPE half from config_schema_gen.go into the single generated module deploy/kcl/config_gen.k (GenerateConfigKCL, the one entrypoint the pipeline calls).

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 entries of a workload's env map (fw.Workload.env, kcl/workload.k) —

  • appConfigEnvMap(c, config_secrets) -> {str: str | forge.SecretRef} keyed by ENV_VAR name, one entry per config field with a non-empty env_var:
  • non-sensitive -> the value read off c, as a string (INLINE: no ConfigMap object or reference). Projected for EVERY caller: no credential, no start-time dependency, so broadcasting it is free.
  • sensitive -> forge.SecretRef {name = c.<field>.name, key = c.<field>.key, store_key = "<ENV_VAR>", optional?} (reads the typed ConfigSecretRef off the value. Projected ONLY when `config_secrets` names it — see renderConfigEnvMapNamed for the outage that rule exists to prevent.)

A workload consumes it the native way:

env = appConfigEnvMap(app_config, config_secrets) | { <workload extras> }

— config first, composed with map-merge `|` (last-wins), so a workload extra of the same NAME overrides the config entry, and a duplicate name is structurally impossible.

ONE entry serves every runtime. forge.render lowers a SecretRef to the wire's `secretRef` for a cluster or host workload, and — via `store_key`, the key the env's secret store holds the value under — to a `managedSecret {name = store_key}` for a HOSTED one, carrying `optional` through both. So an author never re-states a credential per runtime.

The per-field metadata (env-var name, sensitive flag, value expression) is BAKED into the generated code from the proto, so the emitted KCL is a straight-line map literal. The runtime Go loader (pkg/config/loader.go, which binds env by each field's env_var) reads the SAME projected env vars, so the two stay coherent by construction.

The default Secret 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 author writes into config.k.

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: it 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.

The runtime Go loader (pkg/config/loader.go), which binds env by each field's `env_var` annotation via descriptor reflection, 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, auth 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 — crd_kcl_gen.go projects a project's Kubernetes CRD Go types (api/<version>/*_types.go) into the KCL manifest module that actually installs those CRDs on a cluster, deploy/kcl/lib/crd_gen.k.

WHY THIS EXISTS, AND WHAT IT REPLACES

A CRD used to be declared twice in a forge project: once as the Go types the controller reads and writes, and once as a hand-authored KCL dict that is what actually reaches the cluster (via Environment.additional_manifests). Keeping those two in agreement was a manual, unaided job, and the failure mode is SILENT and unbounded in time:

The API server prunes any field of a custom resource that the installed
structural schema does not declare. A field present in the Go type but
missing from the installed schema does not error anywhere — the CR
applies cleanly, kubectl reports success, and the controller reads a ZERO
VALUE forever. No build, lint or deploy notices.

A test that walks both halves and reports drift is the usual mitigation, and it is strictly worse than not having two halves: it converts a silent corruption into a loud chore, but the chore still has to be done by hand on every field, and the test can only police the keywords someone taught it. Generating the KCL from the Go types removes the second declaration, so there is nothing left to drift and no gate to maintain.

WHY controller-tools, RATHER THAN OUR OWN AST WALK

The schema semantics here are not ours to invent: which fields are required (the omitempty/+optional rule), how a pointer differs from a value, how metav1.Time and metav1.Condition project, how +kubebuilder:validation:* markers lower to OpenAPI keywords, how +listType=map becomes x-kubernetes-list-type. controller-tools IS that specification, and every kubebuilder project in existence agrees with it.

Re-deriving those rules with a hand-rolled reflect/AST walk means owning a forever-incomplete reimplementation of someone else's spec, where each gap is a silently pruned field. So this drives controller-tools as a LIBRARY and lowers its output to KCL — the projection here is purely syntactic (OpenAPI JSON -> KCL dict literal), carrying no schema opinions of its own.

Driving the library rather than shelling out to the controller-gen BINARY is what makes `forge generate` hermetic: no PATH dependency, no separately installed tool whose version can disagree with the schema it produced, and no "works on my machine" gap between a developer's checkout and CI.

Package codegen — crd_kcl_parse.go is the controller-tools driver: it loads a project's api/<version> packages and projects the CRD Go types into apiextensions CustomResourceDefinition objects.

This is deliberately the ONLY file in forge that imports controller-tools. The rest of the CRD pipeline (crd_kcl_gen.go) works on the resulting apiextensions types, which forge already depends on via controller-runtime. Keeping the heavy, opinionated dependency behind one function means the KCL lowering can be tested with hand-built CRD values and no package loading at all — which matters, because loading real Go packages costs seconds and needs a complete, compiling module on disk.

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 (ingress_k3d_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.

Code generated by forge. DO NOT EDIT. forge:hash=57eb4614e494231c07143ff60042b76e400616c7a4e5954ded6646a5e186da1d forge-owned: regenerated every run — do not edit (forge project 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.

File: internal/codegen/read_only_columns.go

READ-ONLY COLUMNS — the columns a client may not write through the generated CRUD Update.

`// forge:read-only` and `// forge:computed` promise "readable, not client-writable", and the born Create request keeps that promise by omitting the field. The AIP-134 Update request cannot: it carries the WHOLE entity, read-only fields included. Before this projection the generated Update wrote them all back, so a client could set a server-owned column (an invoice straight to PAID, past the RPC that owns the state machine) and, worse because nothing announces it, a client that merely did not echo one reset it — an unset enum to the column DEFAULT, a lifecycle timestamp to NULL, a computed total to 0.

Why the proto, and not a migration declaration

Whether a COLUMN may be rewritten is storage truth and is declared in the migration (forge:immutable, projected to ,skipupdate). This is a different question — may a CLIENT write it through the API — and its answer is the wire marker the author already wrote. It is read from the compiled descriptor on every generate (forge_descriptor.go's fieldHasReadOnlyMarker), the same reading that already gates generate (FindUnsatisfiableColumns) and drives the scaffolded edit form; it is not a birth-only instruction.

Projecting it onto storage instead would be wrong twice over. ,skipupdate would take the write away from the custom RPCs that own these columns and write them through the same repository (a full-replace Update<Entity> in app code would silently stop writing status), and a second declaration in the migration could drift from the marker that shapes the Create request and the edit form — one field, two answers to "may the client write it".

Package codegen — tier_kcl_gen.go generates forge's KCL deploy-tier schemas (kcl/tiers/tiers_gen.k) from the Go spec types in pkg/deploy/v1alpha1.

GO IS THE SOURCE, KCL IS A PROJECTION

The tier specs are declared ONCE, in Go, because they have two consumers that must agree: forge's KCL (what an app author writes) and the control plane's CRD (what the API server stores). Before this generator, the hosted backend was declared four times with three unit systems. Now the KCL schema is derived from the same structural schema controller-tools derives the CRD from, so the author-facing field set and the API server's pruning schema cannot disagree. A field present in Go but missing from the installed schema is SILENTLY PRUNED by the API server, and it reads back as a zero value forever.

HOW

controller-tools' crd.Parser produces an OpenAPI JSONSchemaProps per Go type, with every +kubebuilder marker applied: enum, pattern, min/max, default. This file lowers each one to a KCL schema. Every property becomes a field, required-ness comes from the schema, and a $ref to another package type becomes a reference to that type's generated schema. The validation keywords become `check:` lines. The KCL checks are an AUTHOR-TIME convenience that fails at `kcl run` with a message naming the field. Go's Validate() remains the check every path runs.

Cross-field rules (at most one env channel, which kinds may set which fields, the profile mask, and so on) are not JSONSchema and are not generated. They live in Validate() and run on every deploy. Restating them by hand in KCL would recreate the second declaration this generator exists to delete.

File: internal/codegen/unsatisfiable_columns.go

UNSATISFIABLE COLUMN — a NOT NULL column with no DB DEFAULT that ALSO carries `forge:read-only` is provably impossible to INSERT through the generated CRUD path: the marker's only mechanical effect is a WIRE-shape omission — it strips the field from the born Create request (correct: the client must not set it) — but nothing then FILLS the column. Bun writes the Go zero value, which either violates the NOT NULL/CHECK outright or (worse, for a foreign key) satisfies the Go type check and fails at the database as a constraint violation that reads like a caller-side bug.

All three facts — NOT NULL, DEFAULT (or its absence), and the read-only marker — are things forge already knows at generate time from the joined wire+schema EntityDef (see BuildSchemaEntities), so this fires for every project regardless of whether anyone has ever heard of `forge:fill`.

Declaring `forge:fill=<strategy>` on the column (a COMMENT ON COLUMN catalog marker, schemadef.ColumnMarkerFill) suppresses this: either forge fills it itself (`forge:fill=ulid`, see forge/pkg/crud's Repo.fillULIDColumns) or the author has stamped it in their own handler and is acknowledging the gap (`forge:fill=handler`).

File: internal/codegen/unversionable_entities.go

UNVERSIONABLE ENTITY — a `forge:version` column with no matching field on the entity's wire message is provably broken, and broken in a way that looks like data corruption rather than a misconfiguration.

Optimistic concurrency is a compare-and-swap against the version the CALLER last read: the generated Update carries entity.Version into `WHERE version = ?` and increments it in the same statement (see forge/pkg/crud's Repo.applyVersionGuard). That predicate only means anything if the caller can actually round-trip the value — read it on a Get, hand it back on the Update.

With no wire field there is nothing to round-trip. `<entity>FromProto` builds the entity from the request, the absent field leaves Version at the Go zero, and every Update runs `WHERE version = 0`. The first write against a fresh row succeeds and moves the stored version to 1; every write after that matches no row and fails Aborted — permanently, for that row, with a message telling the caller to re-read and retry when re-reading cannot help. An entity in this state is not racy, it is simply un-updatable after its first edit.

Both facts are known at generate time from the joined wire+schema EntityDef, so this fires for every project that declares the marker, which is the point: the marker's whole promise is that a lost update becomes a loud error, and the failure mode without the wire field is a loud error on writes that never raced at all.

Index

Constants

View Source
const (
	OperatorAPIGroupConst   = "APIGroup"
	OperatorAPIVersionConst = "APIVersion"
)

OperatorAPIGroupConst / OperatorAPIVersionConst are the constant names the operator scaffold emits to record the operator's CRD API coordinates in its own package. `forge scaffold crd` reads them to default a new CRD's group/version to its parent operator's.

View Source
const (
	// APIWorkloadName is the hosted API workload's name: its platform
	// hostname, and what the frontend's API_URL references.
	APIWorkloadName = "api"
	// APIWorkloadIdent is the env-local KCL identifier it is declared as.
	APIWorkloadIdent = "_api"
	// ServerSubcommand is the project binary's all-in-one command
	// (cmd-tree-server.go.tmpl): every service mounted, every worker and
	// operator supervised.
	ServerSubcommand = "server"
)

THE HOSTED API IS ONE WORKLOAD.

A browser reaches a project's API at ONE origin: the scaffolded frontend's Connect transport has one base URL (a call is `/<package>.<Service>/<Method>`, so one origin serves every service), and sign-in answers with an HttpOnly session cookie the browser returns to that origin only. The hosted platform gives every workload its own hostname and routes no paths between them, so an env that hosts each service as its own workload leaves the frontend able to reach one of them — and signed in to none of the others. That is not fixable in the browser: per-service URLs would still strand the cookie.

So a hosted env declares its API once, as `_api`: the project binary's all-in-one `server` command, which mounts every service on one Connect mux and supervises every worker beside it, in one process. It binds `_api` instead of its services and workers. workloads.k still declares each of them, and dev still runs each as its own process.

View Source
const (
	// InternalPackageService is the default: a Service/Deps/New contract
	// package with no boundary claim.
	InternalPackageService = "service"
	// InternalPackageOutboundIO marks a package that claims to call out
	// to a third-party system and serve nothing inbound
	// (`//forge:outbound-io`).
	InternalPackageOutboundIO = "outbound-io"
)

Internal-package kinds. There are exactly two, and the distinction is read off the package's own source rather than a registry: a package either claims to be an outbound I/O boundary (`//forge:outbound-io`) or it does not. Every contract package is Service/Deps/New-shaped — what varies is whether it calls OUT, which is the only part a rule can check. Whether the SERVER constructs it is a separate fact: see Composition*.

View Source
const (
	// CompositionComposed is the default: compose.go constructs it.
	CompositionComposed = "composed"
	// CompositionExternal is `//forge:external-component`: the server has it,
	// hand-constructed onto Infra in internal/app/providers.go.
	CompositionExternal = "external"
	// CompositionNotAComponent is `//forge:not-a-component`: the server does
	// not have it; the CLI, a daemon or another binary builds it.
	CompositionNotAComponent = "not-a-component"
)

How the server composition treats a contract package — the second fact a reporter needs, and the one a reader cannot see from the package list alone. Read off the package's own markers, the same ones compose.go's generator obeys (composedComponents), so a report can never claim the server builds what it does not.

View Source
const (
	// ProtoMarkerEntity tablizes a top-level message at birth. Read by the
	// raw scanner (ScanRawProtoDir) — the message may be brand new and not
	// yet reachable from any RPC, so the compiled descriptor cannot carry it.
	ProtoMarkerEntity = "forge:entity"

	// ProtoMarkerSoftDelete births the entity with a nullable deleted_at
	// column. Implies ProtoMarkerEntity. Read by the raw scanner.
	ProtoMarkerSoftDelete = "forge:soft-delete"

	// ProtoMarkerAppendOnly births the entity without Update/Delete RPCs and
	// with a DB guard rejecting both. Implies ProtoMarkerEntity. Read by the
	// raw scanner.
	ProtoMarkerAppendOnly = "forge:append-only"

	// ProtoMarkerReadOnly keeps a field on the entity and on Get/List but
	// never lets a client write it: omitted from the born Create request,
	// and refused/preserved by the generated CRUD Update on every generate
	// (ReadOnlyColumns). Read by BOTH the raw scanner (at birth) and the
	// descriptor path (fieldHasReadOnlyMarker).
	ProtoMarkerReadOnly = "forge:read-only"

	// ProtoMarkerComputed is ProtoMarkerReadOnly plus a DECLARED OBLIGATION:
	// the field is omitted from the born Create/Update requests exactly as
	// read-only is, and the author is additionally stating that something in
	// the app derives its value. The unwritten-computed-field lint check
	// (internal/cli/lint/lint_computed_fields.go) holds them to it.
	//
	// It exists because read-only alone is silent about the SECOND half. A
	// read-only field forge omits from Create and nothing populates takes the
	// column default — `0` for a money column — and the app shows $0.00 with
	// no error anywhere: no constraint is violated, no test fails, and the
	// only symptom is a human eventually reading a screen. The motivating
	// case was a line item's amount_cents, whose comment promised
	// quantity × unit price and whose rows were all zero.
	//
	// Read by BOTH read-only recognizers (the raw scanner at birth and the
	// descriptor path), which treat it as read-only — a marker that only lint
	// understood would change no generated output and would be a second
	// spelling of nothing.
	ProtoMarkerComputed = "forge:computed"

	// ProtoMarkerGenerated carries a SQL expression, verbatim, as the rest
	// of the comment line: `// forge:generated round(quantity *
	// unit_price_cents)::BIGINT`. Birth emits the field's column as
	// `<col> <type> [NOT NULL] GENERATED ALWAYS AS (<expr>) STORED` — NOT
	// NULL exactly when the field is not `optional`, the same
	// presence-is-nullability rule every other born column follows — and the
	// field is read-only (omitted from the born Create request) because
	// postgres refuses any write to the column anyway.
	//
	// It exists because the db skill's own advice — a value derived from the
	// SAME row is `GENERATED ALWAYS AS (…) STORED` — could not be followed at
	// birth: the field was born `BIGINT NOT NULL DEFAULT 0` and the owned
	// migration was hand-edited immediately afterwards, every time.
	//
	// The expression is copied, never parsed. Birth applies the rendered
	// migration to the shadow database before writing anything, so an
	// expression postgres rejects fails the birth naming this marker's file
	// and line — postgres is the only parser of postgres expressions forge
	// trusts. After birth the marker is inert like every birth marker; the
	// applied schema's GENERATED column is the truth from then on.
	//
	// Read by BOTH read-only recognizers (it is in ReadOnlyProtoMarkers) so
	// the field leaves the write surface on every pass, and by the
	// expression extractors (GeneratedMarkerExpr) in the raw scanner and the
	// descriptor path.
	ProtoMarkerGenerated = "forge:generated"

	// ProtoMarkerSecret keeps the column but strips the field from read
	// responses. Read by the descriptor path (fieldHasSecretMarker) ONLY —
	// the raw scanner has no need for it, which is precisely why the
	// registry must be the union of what every pass reads rather than the
	// vocabulary of any single scanner.
	ProtoMarkerSecret = "forge:secret"

	// ProtoMarkerGuards declares that a CUSTOM RPC owns a column's writes,
	// naming the column as `<table>.<column>`. Written on a FIELD of that
	// rpc's request message; the field it sits on is incidental — it is the
	// attachment point, not the subject. Read by the descriptor path
	// (fieldGuardTargets) and consumed by the frontend page generator,
	// which drops the named column from the scaffolded edit page's
	// update_mask.
	//
	// It exists because the page generator reasons from the ENTITY's column
	// list and can see nothing of what the service's custom RPCs enforce.
	// The measured case: RecordPayment refuses an overpayment, while the
	// scaffolded invoice edit page writes amount_cents raw through
	// UpdateInvoice — lower it below amount_paid_cents and the raw
	// invoices_not_overpaid CHECK surfaces as a 500. The state machine the
	// author wrote is bypassed by the form forge itself generated.
	//
	// The declaration is EXPLICIT because inference was tried and is
	// unsound. RecordPaymentRequest carries `invoice_id` and
	// `amount_cents`, yet guards `invoices.amount_paid_cents` — a column
	// its request never names — while `amount_cents`, the field a
	// name-matcher WOULD latch onto, is a freely editable column on a
	// different table. Matching request field names against entity columns
	// would therefore miss the real guard AND freeze an unrelated column,
	// both in silence. That is strictly worse than the defect it set out to
	// fix, which is why the marker names its target rather than implying it.
	ProtoMarkerGuards = "forge:guards"

	// ProtoMarkerMutation forces an rpc's generated React Query hook to be a
	// useMutation. Read by the frontend hook generator on EVERY generate,
	// not at birth — the one marker here that is not an entity-birth
	// concern, and another reason the registry cannot be derived from the
	// birth scanner alone.
	ProtoMarkerMutation = "forge:mutation"

	// ProtoMarkerCustom declares that ONE rpc is a custom rpc, whatever its
	// name. An rpc is otherwise CRUD when it is unary, named exactly
	// Create<E>/Get<E>/Update<E>/Delete<E>/List<Es>, and <E> has both a table
	// and a wire message — a rule about NAMES, which cannot see that
	// CreateTask also wires the task's dependencies, or that ListTasks reads a
	// join. Marked, the rpc is custom on both sides of the wire: it gets a
	// pb-through stub and a scaffold test row, no CRUD shim or op, no entity
	// page, its hook invalidates at service scope, and it makes no entity — so
	// when every CRUD-named rpc of an entity carries it, there is no entity
	// at all (no ORM struct, no page, no seed root), which is the per-entity
	// opt-out, spelled one rpc at a time.
	//
	// It exists because the alternative the framework guide forbids —
	// renaming the rpc to dodge detection — was the only way to say this. A
	// project adopting forge with rpcs that are CRUD in name only (reliant had
	// 52) could not adopt a service without forge wiring generic CRUD over
	// its business logic.
	//
	// A comment marker rather than a (forge.v1.method) option, for the reason
	// ProtoMarkerUnmanaged gives: auth_required and errors are facts about
	// the API CONTRACT, which every client reads off the descriptor; whether
	// forge or the author implements the handler is not, and must not cost a
	// project the forge.proto import to declare.
	//
	// Read by the descriptor path (Method.Custom) from the rpc's leading
	// comment blocks — the attached one and any detached one between it and
	// the previous declaration — and by the raw scanner (RawProtoRPC.Custom)
	// from every comment line above the rpc. Never from a trailing comment:
	// protobuf hands a block written under one rpc for the next to the one
	// above it, or drops it, so generate refuses a trailing marker and a
	// marker the source shows but the descriptor lost. A near-miss spelling
	// on an rpc, or the marker on a service or message, fails generate too.
	ProtoMarkerCustom = "forge:custom"

	// ProtoMarkerUnmanaged takes a whole SERVICE out of forge's service
	// model: buf still generates its Go/TS wire code and its descriptor
	// entry, but forge scaffolds and generates nothing service-scoped for
	// it — no internal/handlers/<svc>/, no rpc stubs or scaffold tests, no
	// mocks, CRUD wiring or entities, no mount, compose entry or
	// subcommand, no frontend hooks or pages — and every audit and lint
	// that checks a service's handlers skips it. Deleting the line adopts
	// the service; the next `forge generate` scaffolds it like any other.
	//
	// It exists so a project with handlers that already live somewhere
	// else can adopt forge one service at a time. Without it the first
	// generate scaffolds a stub for every rpc of every service at once.
	//
	// The one projection that still covers an unmanaged service is the
	// open-procedure set (pkg/middleware/procedures_gen.go): auth_required
	// is a fact about the API contract, not about who implements the
	// handler, and a declared-public rpc must not fail closed because its
	// handler is mounted by hand.
	//
	// Read by the descriptor path (ServiceDef.Unmanaged) and by the raw
	// scanner (RawProtoService.Unmanaged), from the comment block above
	// `service X {`, with the entity markers' placement rule.
	ProtoMarkerUnmanaged = "forge:unmanaged"
)

The markers a .proto comment may carry. Each names the recognizer that consumes it, because they are not all consumed by the same pass — a fact the registry has to cover or the lint check would flag a valid marker whose reader it did not know about.

View Source
const (
	WorkloadKindService  = "service"
	WorkloadKindWorker   = "worker"
	WorkloadKindCron     = "cron"
	WorkloadKindJob      = "job"
	WorkloadKindOperator = "operator"
	WorkloadKindTool     = "tool"
)

The KCL workload kinds — how a deployable unit RUNS. These are the values of `forge.workloads.Workload.kind` and are deliberately a different axis from config.ComponentKind*, which describes what forge SCAFFOLDS (whether a component gets a proto, a cmd/ dir, bootstrap wiring). Two components of different Go kinds can deploy identically; WorkloadKindFor is the mapping.

View Source
const CRDAPIDir = "api"

CRDAPIDir is the conventional location of a forge project's CRD Go types.

View Source
const CRDKCLModule = "crd_gen"

CRDKCLModule is the basename (no extension) of the generated KCL module. Consumers do `import lib.crd_gen` and call `crd_gen.<lower>_crd()`.

View Source
const ConfigSchemaModule = "config_gen"

ConfigSchemaModule is the KCL module name (and filename stem) the config projection is emitted under: deploy/kcl/config_gen.k. It carries BOTH halves — the TYPE (`schema AppConfig` / `schema ConfigSecretRef`, emitted here) and the BEHAVIOR (`appConfigEnvMap`, emitted by config_projection_gen.go) — because both are projected from the same proto by the same generate run and are only ever correct together.

Each per-env values file (config.k) and each env's main.k import it by THIS name and qualify the symbols as `config_gen.AppConfig`, `config_gen.appConfigEnvMap` etc. — KCL does not share top-level symbols across separately-imported modules, so the reference must be qualified.

The `_gen` suffix is forge's Tier-1 naming convention: a file forge rewrites on every run says so in its own name.

View Source
const ControllerToolsVersion = "v0.20.1"

ControllerToolsVersion is the controller-tools release whose schema semantics this build projects under.

It is stamped into every generated CRD's controller-gen.kubebuilder.io/version annotation. It must be updated in lockstep with the controller-tools version in forge's go.mod — a stale value here would misreport which rules produced a schema, which is exactly the provenance question someone asks after a bump changes how a Go construct projects.

View Source
const DeepCopyFile = "zz_generated.deepcopy.go"

DeepCopyFile is the file controller-tools' object generator owns in each API package.

View Source
const DefaultConstructorName = "New"

DefaultConstructorName is the constructor name forge assumes when a package declares no `// forge:constructor` marker — the zero-annotation shape every scaffold is born with.

View Source
const DevEnvName = "dev"

DevEnvName is the env the scaffold binds to the local loop (host processes plus the local k3d cluster). Every other env is hosted on the forge control plane as scaffolded (deploy/kcl/cloud/main.k.tmpl).

View Source
const ForgeTierAPIPackage = "github.com/reliant-labs/forge/pkg/deploy/v1alpha1"

ForgeTierAPIPackage is forge's own deploy-tier API package (group forge.dev). A project that imports it RUNS those kinds (the control plane's operators do), so its CRDs must be installed from forge's types rather than re-declared under the project's api/.

View Source
const FrontendConfigGlobal = "__FORGE_CONFIG__"

FrontendConfigGlobal is the browser global the runtime document assigns and the generated module reads.

View Source
const FrontendConfigJSFile = "config.js"

FrontendConfigJSFile is the runtime config document's path inside a frontend, relative to the frontend root. It lands in the framework's static-asset directory so it is served verbatim at <basePath>/config.js.

View Source
const FrontendConfigModule = "frontend_config_gen"

FrontendConfigModule is the generated KCL module name carrying every frontend's typed schema plus its runtime-config projection.

It is a SEPARATE module from config_gen.k (the backend's) because the two have different audiences and different rules: config_gen.k models secret references and projects to a workload's env, while this one models a strictly public surface and projects to a JSON document served to a browser. Keeping them apart makes the boundary legible in the file tree — anything in here is public by construction.

View Source
const FrontendConfigTSFile = "src/lib/config_gen.ts"

FrontendConfigTSFile is the generated module's path inside a frontend, relative to the frontend root.

View Source
const IdentityKCLRelPath = "deploy/kcl/identity.k"

IdentityKCLRelPath is the one ManagedIdentity declaration every env imports.

View Source
const MigrateWorkloadName = "migrate"

MigrateWorkloadName is the name of the scaffolded migration workload. It is a workload name like any other, so it must be a legal DNS-1123 label — which is also why it can never collide with the `*` broadcast selector.

View Source
const ResolvesCallerDirective = "forge:resolves-caller"

ResolvesCallerDirective declares that calling a function resolves the caller: it reads the authenticated principal the auth interceptor put on the request context. It goes in the function's doc comment:

// GetUserIDFromContext returns the signed-in user's id.
//
//forge:resolves-caller
func GetUserIDFromContext(ctx context.Context) (string, bool)

Forge's own seam (CRUDAuthSeam, middleware.GetUser in the scaffolded pkg/middleware) needs no marker. A project with its own auth package — an adopted one, whose interceptor predates forge — marks its accessors instead, and everything that asks "does this code resolve the caller?" (the unscoped_auth audit first) accepts them. The declaration sits beside the code it describes, like //forge:no-orm, rather than in forge.yaml, which never mentions the package.

Package-level functions only. A method cannot be matched at a call site without type-checking (the call names no receiver type), and matching it by name alone would clear handlers on coincidence.

View Source
const ScaffoldPlaceholderRegistry = "REPLACE-ME-REGISTRY.invalid"

ScaffoldPlaceholderRegistry is the registry host a scaffolded image carries when forge cannot derive one. See ScaffoldImageRef for why it is shaped like a host and why the TLD is `.invalid`.

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.

View Source
const TierKCLPath = "kcl/tiers/tiers_gen.k"

TierKCLPath is where the generated module lives, relative to forge's repo root. It is embedded with the rest of the KCL module and imported by projects as `forge.tiers`.

View Source
const TierSpecPackage = "./pkg/deploy/v1alpha1"

TierSpecPackage is the Go package the tier specs live in.

View Source
const UnwiredStubMarkerPrefix = "forge:gen unwired-stub symbol="

UnwiredStubMarkerPrefix is the literal that opens the marker comment. Every emitter builds its marker from UnwiredStubMarkerComment (which is built from this), and UnwiredStubMarkerRE reads it back, so the marker text exists in exactly one place.

View Source
const WorkloadsKCLRelPath = "deploy/kcl/workloads.k"

WorkloadsKCLRelPath is the project-relative path of the workload declaration — what the project DEPLOYS, one typed literal per deployable unit (an image + args + placement).

It is SCAFFOLDED ONCE and owned by the project from then on: forge writes it when the file does not exist and appends to it when `forge scaffold` adds a component; it never rewrites or reformats what is already there. Deploy has exactly one source of truth, it is KCL, and it is tracked in git — so a fresh clone renders with nothing generated first.

Variables

KnownProtoMarkers is the complete set of `forge:*` marker names a .proto comment may carry, spanning every pass that reads one (entity birth, descriptor projection, frontend hook generation). It is the single source of truth shared by `forge project annotations` (internal/cli/annotations.go) and the unknown-proto-marker lint check (internal/cli/lint), mirroring schemadef.KnownColumnMarkers on the SQL side so that neither the docs nor the linter can drift from what the scanners actually recognize.

ReadOnlyProtoMarkers are the markers that exclude a field from the born Create/Update request messages. ProtoMarkerComputed is read-only PLUS a declared derivation obligation, so every pass that honours read-only must honour it identically — a marker that changed no generated output would be a second spelling of nothing, and a field marked computed but still client-writable is the exact silent-write hole read-only exists to close.

ProtoMarkerGenerated is here for the same reason: postgres refuses every write to a GENERATED column, so a field still carried on the Create request would be a client-writable value the database rejects on insert.

Exported as a set rather than left as independent regexes because the implication has to hold in BOTH recognizers (the raw scanner at birth and the descriptor path). Two hand-maintained lists is how one of them ends up honouring a marker the other ignores, which would make a field's writability depend on which pass found it.

View Source
var RemovedProtoMarkers = map[string]string{

	"forge:server-set": ProtoMarkerReadOnly,
}

RemovedProtoMarkers maps a spelling forge USED to recognize onto the marker that replaced it. Entries here are inert — nothing in any scanner consults this map, and a proto carrying one of these keys behaves exactly as if it carried arbitrary prose, which is the point. It exists so the lint check can say "renamed to X" instead of the far less actionable "unrecognized", turning the one diagnostic an author gets into a runbook.

Adding a key here must never be mistaken for restoring the alias: the contract is that the old spelling stays dead and only the MESSAGE about it improves.

View Source
var TierRoots = []string{"WorkloadSpec", "StaticSiteSpec", "ManagedDatabaseSpec"}

TierRoots are the spec types whose KCL schemas are generated. Every type they reference is generated too, transitively. The KCL schema name is the Go type name minus the "Spec" suffix, which is what an author writes (tiers.Workload { ... }).

View Source
var UnwiredStubMarkerRE = regexp.MustCompile(`//\s*` + regexp.QuoteMeta(UnwiredStubMarkerPrefix) + `(\S+)`)

UnwiredStubMarkerRE matches the marker line `// forge:gen unwired-stub symbol=<pkg>.<Method>` that every unwired-stub emitter stamps, capturing the symbol. It is the single source of truth shared by the excision pass (unwired_stub_excise.go) and `forge project audit` (orphan-stub category).

Functions

func APIWorkloadStanza added in v0.1.45

func APIWorkloadStanza(modulePath, projectName string, extraSecrets ...string) string

APIWorkloadStanza renders a hosted env's `_api` declaration: the project binary's `server` as one service workload. The facts are the ones WorkloadStanza gives every component of that binary — the image, the build, the `http` port, the credential the DI graph reads — with `server` as the subcommand, so `_api` and the services it runs cannot disagree about them.

extraSecrets are credentials THIS env hands the API beyond the database — the dev env's login-broker token, which only an env running the dev IdP has. They are an env fact, not a workload fact, so the caller passes them.

func AppendWorkloadStanza added in v0.1.45

func AppendWorkloadStanza(projectDir, modulePath, projectName string, c config.ComponentConfig) (applied bool, err error)

AppendWorkloadStanza adds one workload to a project's deploy/kcl/workloads.k — its declaration, and its name in the `ALL` list — and reports whether it did.

ADDITIVE ONLY. Existing content is never rewritten, reformatted or reordered: the file is user-owned, so the only edits forge makes to it are the new declaration (above the ALL list's comment block) and the one new list entry, written in the style the list already uses. Every other byte, comment and line break stays where the user left it.

It returns applied=false, with no error and no write, when the file is missing, the workload is already declared, or the ALL list cannot be located unambiguously. The caller then PRINTS the stanza for the user to paste.

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 AssignsContain added in v0.1.45

func AssignsContain(assigns []string, tok string) bool

AssignsContain reports whether any of the create-path assignments contains tok. The create closure is built per METHOD, not per entity, so its import needs are gated separately from the conversion pair's.

func AttachEntityMeta

func AttachEntityMeta(page *PageTemplateData, entity EntityDef, svc ServiceDef)

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. svc supplies the deep type graph needed to resolve enum COLUMNS to their protobuf-es TS type (which the badge cell passes to StatusBadge); an unresolvable enum column degrades to the enum-like string path (String(item.field)).

func AttachForeignKeys added in v0.1.45

func AttachForeignKeys(page *PageTemplateData, referents map[string]PageFieldFK)

AttachForeignKeys marks every create/update form field and every detail column of `page` that is a foreign key onto one of `referents`.

A field qualifies when its proto name ends in `_id`, the remaining owner resolves to a referent, and the field is a plain string — an integer `*_id` is not pointing at a forge entity (whose primary key is TEXT), and a repeated or enum field is not a single reference.

A self-reference (`parent_id` on Category resolving to Category) is left alone: the picker would browse the very rows being edited, and a tree parent picker is a domain decision, not a generated default.

func BuildEntityConv

func BuildEntityConv(svc ServiceDef, entity EntityDef) (EntityConvTemplateData, []UnmappedField)

BuildEntityConv builds the conversion data for one entity, plus every (wire field, column) pair it could not map.

func BuildFKReferents added in v0.1.45

func BuildFKReferents(pages []PageTemplateData) map[string]PageFieldFK

BuildFKReferents indexes the project's CRUD pages by the entity name a `<owner>_id` field would name, so field resolution is a map lookup.

Pages MUST have been through AttachEntityMeta — DisplayField and PkFieldCamel come from there. An entity without BOTH a List and a Get RPC is skipped: the picker browses with List and the closed-state label resolves with Get, so a half-CRUD entity cannot back one.

func CRDLambdaName added in v0.1.45

func CRDLambdaName(kind string) string

CRDLambdaName maps a CR kind to its generated KCL lambda name: "ManagedDatabase" -> "manageddatabase_crd". Lowercasing the whole kind (rather than snake-casing it) matches the hand-authored spelling this generator replaces, so an existing project's call sites keep working unchanged.

func CRDSourceRoots added in v0.1.45

func CRDSourceRoots(projectDir string) ([]string, error)

CRDSourceRoots returns the package patterns whose CRD types a project installs:

  • "./api/..." when the project has an api/ directory (its own kinds);
  • ForgeTierAPIPackage when the project REGISTERS forge's tier kinds, i.e. it references that package's AddToScheme.

The second source is derived from the code rather than configured. That is deliberate: registering the kinds in a scheme is exactly what a process that reconciles them must do, so a separate opt-in could only disagree with it. A reconciler whose CRD is never installed fails only at runtime, with "no matches for kind". A plain IMPORT is not the signal. A package that only uses a constant or Validate() from the tier types (the secret materializer does) does not reconcile anything, and treating that as "install the CRDs" collided with a project's own same-named kinds.

func CRUDAuthSeam added in v0.1.45

func CRUDAuthSeam() string

CRUDAuthSeam returns the qualified app-owned function a handler calls to demand a principal ("middleware.GetUser"). It is the same value the scaffold renders into every CRUD shim header, exported so analyses that ask "does this handler resolve the caller?" resolve the seam from the generator rather than hardcoding a name the generator could rename out from under them.

func CRUDAuthSeamFuncs added in v0.1.45

func CRUDAuthSeamFuncs() map[string]bool

CRUDAuthSeamFuncs is the set of function names that mean "this code resolved the caller" — the seam itself plus the claims accessor the seam package re-exports. Returned as a fresh map so callers cannot mutate the generator's view of its own seam.

func CRUDDelegatePkgName added in v0.1.45

func CRUDDelegatePkgName() string

CRUDDelegatePkgName is the package selector a delegating CRUD shim calls through ("crud", the last segment of crudLibImport). Derived from the import the shim actually stamps, so moving the delegate package moves this with it.

func CRUDProjectionReferences added in v0.1.45

func CRUDProjectionReferences(dir string, methods []Method) (map[string]CRUDProjectionReference, error)

CRUDProjectionReferences reports, per method (keyed by Go method name), the first place the handler package at dir uses a symbol forge generates for that rpc only while it is CRUD: its op (crud<Method>Op), and for a Create-named rpc its create-request factory (New<Request>), which the scaffolded lifecycle test calls. Tests are read too — a test calling either breaks `go test` as surely as a handler breaks the build. Forge's own generated files (handlersGeneratedFiles, *_gen.go, *_gen_test.go) are not read: they DECLARE these symbols, they are not demand for them. Methods nothing references are absent. A missing dir is no references.

It answers the question a `// forge:custom` rpc raises when it was CRUD before: forge stops generating both, so code still using them fails after the whole generate ran.

func CRUDShape added in v0.1.45

func CRUDShape(m Method) (operation, entityName string)

CRUDShape is the one answer to "does this rpc's DECLARATION claim CRUD, and on which entity name?" — every projection that classifies an rpc asks it, so the server wiring, the entity set, the frontend, the seed scope and the audit cannot disagree about one rpc.

An rpc claims CRUD when it is unary, named Create<E>/Get<E>/Update<E>/ Delete<E>/List<Es>, and carries no `// forge:custom` marker (ProtoMarkerCustom). The claim is not the verdict: the rpc is CRUD only when <E> also has a table and a wire message, which MatchCRUDMethods and BuildSchemaEntities decide. Everything CRUDShape refuses is custom.

The entity name is as written — List's plural is NOT singularized here, because each caller resolves it against a different inventory.

func CanonicalGoTypeOK added in v0.1.45

func CanonicalGoTypeOK(canonical string, isArray bool) (goType string, ok bool)

CanonicalGoTypeOK maps a canonical schema type (the closed vocabulary schemadef.MapDeclaredType produces) to the Go type a column projects to on the generated entity struct — the NOT NULL variant; a nullable column adds a pointer, and an array column never does because a slice already carries nil.

This is the SINGLE definition. There used to be three: this one, the CRUD conversion generator's dbBaseGoType, and internal/generator's planFieldGoType. They disagreed exactly where it mattered — dbBaseGoType collapsed every array column that was not BIGINT[] to []string, so a BYTEA[] column projected to []string in the half that decides whether a wire field pairs with it and to something else in the half that declares the struct field. Two projections of one column is one projection too many; the other two now route here.

A json ARRAY column (jsonb[]) projects to []string: each element is a document, the same way a scalar json column projects to the document text.

ok=false means the string is not a canonical schema type and the "string" answer is a FALLBACK rather than a mapping — the same distinction, for the same reason, that schemadef.MapDeclaredType draws with its `known` return. Every caller holds a type that is supposed to be canonical, so ok=false is a forge bug and each one reports it by name rather than declaring the field as text.

The `default:` arm this used to end with covered "string, json, unknown" in one breath, which is the defect in miniature: `string` and `json` are MAPPED to Go string, and "unknown" is a type forge has no projection for. Collapsing the three meant a canonical type added upstream without a projection here became a Go `string` field in silence.

func CheckDuplicateConfigFields added in v0.1.45

func CheckDuplicateConfigFields(fields []ConfigField, module, schemaName string) error

CheckDuplicateConfigFields refuses a field set that would declare the same field name twice inside ONE generated KCL schema.

KCL accepts a repeated field in a schema suite silently and the LAST declaration wins, so the first field's default is discarded with no diagnostic. That is not a style problem — it is a value the operator never wrote appearing in a rendered manifest. Observed in production: a config proto declared `app_url = "http://localhost:3000"` and a second config proto, flattened into the same AppConfig, declared `app_url = ""`. The empty string won, three prod workloads shipped `APP_URL: ”`, and the consumer gated a browser auth redirect on the value being non-empty — so the collision silently downgraded that redirect to a bare 401. Nothing in any diff showed it, because config_gen.k is generated and nobody reads it.

The refusal is per SCHEMA, never across schemas. A project with per-binary configs emits AdminConfig, GatewayConfig and so on into the same module, and those legitimately share field names — `port` on both binaries is two separate declarations in two separate suites, which is the whole point of splitting them. Only a repeat WITHIN one suite shadows anything.

Block-reference fields (MessageType set) are exempt: they project to a comment rather than a schema field, so two of them cannot shadow each other. They are still checked against SCALAR fields of the same name, because that pair does produce one real declaration plus a confusing comment, and it means the proto author has two different fields fighting over one name.

module names the KCL module the schema lands in (ConfigSchemaModule, FrontendConfigModule, …) so the message points at the file that would actually have carried the shadowed field.

func ClosestFrom added in v0.1.45

func ClosestFrom(name string, vocabulary []string) string

ClosestFrom returns the entry of vocabulary nearest to name by Levenshtein distance, or "" when nothing is close enough to be worth suggesting. The cutoff scales with the name's length (a third of it, minimum 1), so a genuine typo earns a suggestion while an unrelated token does not get an arbitrary entry pinned on it.

Exported as the vocabulary-agnostic core of ClosestProtoMarker so the checks that validate a DIFFERENT forge vocabulary — the annotation OPTION fields in internal/cli/lint/lint_proto_options.go, read off the compiled descriptors — score near-misses by exactly the same rule rather than growing a second, subtly different one.

func ClosestProtoMarker added in v0.1.45

func ClosestProtoMarker(name string) string

ClosestProtoMarker returns the known proto marker nearest to name by Levenshtein distance, or "" when nothing is close enough to be worth suggesting. The cutoff scales with the name's length (a third of it, minimum 1) so a genuine typo — `forge:read-only`, `forge:etnity` — earns a suggestion while unrelated prose that happens to start with `forge:` does not get an arbitrary marker pinned on it.

A removed spelling is deliberately NOT resolved here; RemovedProtoMarkers carries a better answer than edit distance can, and the caller checks it first.

func CmdServiceCommand added in v0.1.45

func CmdServiceCommand(name string) (runtime, ctor string, ok bool)

CmdServiceCommand describes the services/ group command `forge generate` emits for a raw service-name spelling: the runtime (kebab) name — which is both the cobra Use value and the <name>.go filename stem under cmd/<bin>/cmd/services/ — and the exported constructor cmd/<bin>/main.go must name to ship it.

ok is false for a name the command tree RESERVES (see reservedSubcommandNames): those get no subcommand file at all, so there is nothing to wire into the composition root.

It exists so callers that TALK about the command (the `forge scaffold service` wiring instruction) derive the symbol from the same place the generator does, and can never print a constructor that was not emitted.

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 CommentMarksCustom added in v0.1.45

func CommentMarksCustom(comment string) bool

CommentMarksCustom reports whether an rpc's comment text (slashes already stripped, the descriptor path's shape) carries ProtoMarkerCustom.

func CommentMarksUnmanaged added in v0.1.45

func CommentMarksUnmanaged(comment string) bool

CommentMarksUnmanaged reports whether a service's comment text (slashes already stripped, the descriptor path's shape) carries ProtoMarkerUnmanaged.

func ComponentTestHelperRelPath added in v0.1.45

func ComponentTestHelperRelPath(handlerLeaf string) string

ComponentTestHelperRelPath is the project-relative path of a service's generated test-helper file. Exposed so tests (and callers reasoning about generated output) name the location in one place rather than rebuilding the "internal/handlers/<leaf>/helpers_gen_test.go" string each time.

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 a WIRED internal component of the same name (e.g. service `billing` + component `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.

The collision test is IsWiredComponentDir — "does this package occupy a factory-namespace slot" — not "does a directory of this name exist". Those are different questions, and answering the second one made this function disagree with the generator it is supposed to mirror; see IsWiredComponentDir.

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

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 ConfigPackageDeclaresDefaults added in v0.1.46

func ConfigPackageDeclaresDefaults(projectDir, modulePath, importPath string) bool

ConfigPackageDeclaresDefaults reports whether the project package at importPath declares a package-level `func Defaults()` returning one value — the generated pkg/config's constructor for the config Load yields with nothing set (config.go.tmpl). The test harness defaults a component's config to it when it exists. It is read from the source rather than assumed from the path: a CLI's stub pkg/config, a hand-written one, or a config package outside pkg/config declares Config without it, and a harness calling a function that does not exist would not compile.

The config loader step runs before the harness step, so a generate that (re)writes pkg/config/config_gen.go is seen by the harness in the same run.

func ConvNeedsFmt added in v0.1.45

func ConvNeedsFmt(convs []EntityConvTemplateData) bool

ConvNeedsFmt reports whether any conversion emits a guard that calls fmt.Errorf — the corrupt-enum read, or either half of a json/jsonb pairing. The ops file imports fmt only then; otherwise the import would be unused. (goimports would also correct this post-write, but gating it keeps the rendered file correct on its own.)

func ConvNeedsORM added in v0.1.45

func ConvNeedsORM(convs []EntityConvTemplateData) bool

ConvNeedsORM reports whether any conversion calls into pkg/orm — today the json/jsonb encoders. Gated for the same reason as ConvNeedsFmt: an entity with no json column must not pull in the import.

func ConvNeedsTime added in v0.1.45

func ConvNeedsTime(convs []EntityConvTemplateData) bool

ConvNeedsTime reports whether any conversion reaches the stdlib `time` package — today only the legacy-TEXT timestamp pairing, which formats and parses time.RFC3339Nano. Gated for the same reason as ConvNeedsFmt: an entity with no such column must not pull in the import.

func ConvNeedsTimestamppb

func ConvNeedsTimestamppb(convs []EntityConvTemplateData) bool

ConvNeedsTimestamppb reports whether any assignment uses timestamppb. Comment-only entries (the wire-only and forge:secret explanations) are ignored: a comment naming *timestamppb.Timestamp must not pull in an import no code uses.

func CustomMarkerNearMiss added in v0.1.45

func CustomMarkerNearMiss(comment string) string

CustomMarkerNearMiss returns the first `forge:*` token in an rpc's comment that is a misspelling of ProtoMarkerCustom (`forge:cusotm`, `forge:custom-rpc`), or "" when there is none. Refused for the reason UnmanagedMarkerNearMiss gives: a misspelled forge:custom leaves the rpc CRUD, and the next generate wires generic CRUD over the logic the marker was written to protect.

func DeclaredCallerResolvers added in v0.1.45

func DeclaredCallerResolvers(projectDir string) ([]CallerResolver, []IgnoredResolverMarker)

DeclaredCallerResolvers finds every function in projectDir's main module declared with ResolvesCallerDirective. It reads non-test .go files only, and skips hidden directories, node_modules, vendor, testdata and nested modules (a directory with its own go.mod is another module's code). A file is parsed only when its bytes mention the directive, so the walk costs little more than reading the tree.

Results are sorted by import path, then name. A project without a go.mod declares nothing.

func DeclaredWorkloads added in v0.1.45

func DeclaredWorkloads(src string) map[string]DeclaredWorkload

DeclaredWorkloads parses each workload literal in a workloads.k, keyed by its declared name.

Docstrings and comments are stripped first (StripKCLProse), because the scaffolded workloads.k DOCUMENTS the shape it expects, including worked examples, and an example is not a declaration. A literal this cannot read is simply absent.

A build bound to a top-level variable (`build = _cp_build`, where `_cp_build = forge.GoBuild {...}`) is resolved, because stating the one project build once and naming it from every workload is how a project with several workloads on one binary writes it.

func DeclaresServes added in v0.1.45

func DeclaresServes(declared map[string]DeclaredWorkload) bool

DeclaresServes reports whether any workload in the file declares `serves` — the project's own statement that it runs several components through one subcommand. In such a project a new component may belong to one of those processes, which forge cannot see, so `forge scaffold` shows the choice instead of declaring a workload the project most likely does not want.

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 DepsLiteralKeys added in v0.1.45

func DepsLiteralKeys(dir string) (keys map[string]bool, found bool)

DepsLiteralKeys returns every field name a `<pkg>.Deps{…}` composite literal may legally key for the Deps struct declared in dir, plus whether a Deps struct was FOUND there at all.

This is a DIFFERENT question from ParseServiceDeps, and the difference is the whole point. ParseServiceDeps answers "what must forge WIRE?" — so it drops unexported and embedded fields, which forge cannot address. DepsLiteralKeys answers "what may the literal legally NAME?" — the full key set, embedded and unexported included. compose.go's reconciler uses it to drop a key that is provably not a field, and it must never mistake a key forge does not emit (an embedded `Base:` the author wired by hand) for a dead one. The wiring set is a subset of the key set, so the two can never disagree about a key forge itself emitted.

The `found` bool is the guard that keeps the removal case from becoming a licence to destroy. An empty key set is AMBIGUOUS: it means either "the Deps struct genuinely has no fields" or "the package is mid-edit and did not parse" — this walk, like ParseServiceDeps, skips a file it cannot parse. Only `found` distinguishes them, and a caller that deletes on the strength of an absent field must act only on a set that was positively read.

func DescriptorSourceHash added in v0.1.45

func DescriptorSourceHash(projectDir string) string

DescriptorSourceHash fingerprints every .proto under <projectDir>/proto by path and content. Order-independent by construction (paths are sorted), so the same tree always hashes the same regardless of walk order.

Content, not mtime: a checkout, a rebase or a `cp -R` rewrites mtimes without changing meaning, and would invalidate a descriptor that is perfectly good. Hashing the bytes answers the question actually being asked — "was this extracted from THESE protos?"

func DetectConstructorName added in v0.1.45

func DetectConstructorName(dir string) string

DetectConstructorName returns the name of the package's component constructor: the `// forge:constructor`-marked func under whatever name its author chose, else `New`.

This is the EMIT-side counterpart of IsComponentConstructor. Freeing the name on the discovery side alone would be worse than not freeing it at all: lint would bless `// forge:constructor func Open(Deps) (Mailer, error)` while the injector kept emitting `pkg.New(pkg.Deps{...})`, so `forge generate` would write a compile-broken internal/app/compose.go pointing the user at generated code they don't own. Detection and emission read the same marker.

Falls back to DefaultConstructorName whenever the directory is missing, unparseable, or declares no constructor — the caller then emits the shape a freshly scaffolded package has, which is the correct guess.

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 DetectDepsConfigImport added in v0.1.45

func DetectDepsConfigImport(dir string) string

DetectDepsConfigImport returns the import path of the package that declares the type of the component's `Deps.Config` field, when that field is a pointer to a type named Config from another package (`Config *config.Config`, usually the project's generated pkg/config). It returns "" when Deps has no such field.

The test harness hands a component a config only through this field, so this is the one fact that decides whether the harness needs a config import at all, and which. It used to import `<module>/pkg/config` unconditionally: a project with no typed config (no proto/config, so no pkg/config) got a helpers_gen_test.go that did not compile for every component, whether or not any of them took a config.

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 DetectObserveChainSeam added in v0.1.45

func DetectObserveChainSeam(dir string) bool

DetectObserveChainSeam reports whether the component package at dir declares the OWNED observability seam `func newObserveChain(...) ...` in a non- generated, non-test source file. Its presence is the per-package opt-in signal for the generated decorator (and, paired with an interface-returning New, for the wrapped compose emission).

Returns false for a missing/unparseable dir. Generated (_gen.go) and test files are ignored, so the generated decorator's own reference to the seam never counts as the seam itself.

func DetectServiceInterfaceName added in v0.1.45

func DetectServiceInterfaceName(dir string) string

DetectServiceInterfaceName returns the name of the package's Service contract interface — the type key a consumer's by-type Deps field is matched against, and the interface the contract-name lint accepts:

  • "Service" — a type named `Service` exists (interface OR struct): the zero-annotation canonical path, unchanged and always preferred.
  • <name> — no `Service` type, but an interface carries the `//forge:service` / `//forge:contract` marker: the marker frees the interface's name (Gateway / Provider / Dispatcher …).
  • "" — neither; the caller defaults to "Service".

The canonical `Service` always wins over a marker (a package with both keeps `Service`), so existing projects are byte-for-byte unaffected. Among multiple marked interfaces the first in deterministic order (file name, then source position) is chosen; marking two interfaces is a user error the lint can surface separately. Non-test, non-generated files only. Unreadable/unparseable dirs return "".

func DiscoverInternalPackageNames added in v0.1.45

func DiscoverInternalPackageNames(projectDir string) ([]string, error)

DiscoverInternalPackageNames is DiscoverInternalPackages projected to the nested names the bootstrap/injector codegen keys on.

func EnsureFrontendConfigInstances added in v0.1.45

func EnsureFrontendConfigInstances(configs []FrontendConfig, kclDirAbs, envName, projectName string) ([]string, error)

EnsureFrontendConfigInstances makes sure an env's user-owned config.k declares one typed VALUES instance per frontend config — the half the runtime projection reads.

It appends rather than scaffolding a whole file, because config.k is shared with the backend's app_config and is written once (write-if-absent). A frontend annotated AFTER the env was created — the normal order, since a project scaffolds its envs long before it declares a frontend config — would otherwise never get an instance, leaving the projection nothing to project and every environment silently on proto defaults.

Only MISSING instances are appended, matched by schema name, so this is idempotent and never touches values an author has edited. Returns the frontends it added an instance for.

func EntityConstraintsToPlan added in v0.1.45

func EntityConstraintsToPlan(cs []EntityConstraint) []config.PlanEntityConstraint

EntityConstraintsToPlan converts introspected constraints to the plan shape the ORM generator consumes.

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 EntityRPCs added in v0.1.46

func EntityRPCs(projectDir string, services []ServiceDef, entities []string) map[string][]EntityRPC

EntityRPCs returns, for each entity name asked about, the declared rpcs that claim it — by the same rule entitiesForTables applies, so they are exactly the rpcs whose `// forge:custom` would make the table plain schema. It is what the advice for keeping a table off forge's ORM must name: the rpcs the project HAS, not the five a CRUD quintet would have.

func EntityWireKey added in v0.1.45

func EntityWireKey(e EntityDef) (field string, ok bool)

EntityWireKey is the field the frontend keys an entity's rows by, and ok=false when the wire message offers no real key. In order: the DB primary key when the message carries it, else the conventional `<singular table>_id` domain key (a table can hold a surrogate `id` while the published proto exposes `usage_event_id` instead). Picking "the first field" or a literal "id" is a guess, not a key, and is deliberately not offered here.

func EnvBinder added in v0.1.45

func EnvBinder(env, kind string) string

EnvBinder names the binder a freshly scaffolded env uses for a workload of kind. In the local loop: a host process where one can run, and the local cluster where it cannot (a cron is scheduled by Kubernetes, an operator watches its API). Elsewhere: the forge control plane for every kind it admits, and a cluster you operate for the kinds it refuses (HostedRefusal). A tool is never run anywhere.

func EnvBinding added in v0.1.45

func EnvBinding(env, kind, workloadName string) string

EnvBinding is one line of an env's `_workloads` list, as a fresh scaffold writes it.

func EnvBindingHint added in v0.1.45

func EnvBindingHint(env, kind, workloadName string) string

EnvBindingHint is the message shown when a binding could not be appended.

func EnvDeclaresNoCluster added in v0.1.45

func EnvDeclaresNoCluster(projectDir, env string) bool

EnvDeclaresNoCluster reports whether an env's main.k still carries the scaffold's `_cluster = None`: a workload bound `_on_cluster` there fails the render, naming itself, until the author declares the cluster.

func EnvSecretsFileName added in v0.1.45

func EnvSecretsFileName(envName string) string

EnvSecretsFileName is the per-env secret store the `file` secret provider reads: `secrets/<env>.yaml`, keyed by ENV-VAR NAME (the convention internal/secrets resolves on). It is gitignored — it holds secret VALUES, the half that never lives in git.

It is deliberately NOT a dotenv. The `dotenv` provider was removed (it handed every host service the whole file, so a value went live without ever being declared in KCL), and `forge lint`'s no-dotenv rule now REJECTS a `.env*` file. A scaffold that still wrote one failed its own linter on the first run of a brand-new project.

func EnvServerWorkload added in v0.1.45

func EnvServerWorkload(content, projectName string) (string, bool)

EnvServerWorkload returns the identifier of a workload an env's main.k declares FOR ITSELF that runs the project binary's `server` — a hosted env's `_api`. Read from the source, like DeclaredWorkloads, so scaffold and `forge env new` need no render. Comments and docstrings are stripped first, so the worked examples in a scaffolded file are not mistaken for one.

func ExciseUnwiredStubs added in v0.1.45

func ExciseUnwiredStubs(projectDir, dir string, methods map[string]bool) ([]string, error)

ExciseUnwiredStubs removes, from the user-owned handler files in dir, every *Service method whose name is in `methods` that is still a pristine forge-emitted unwired stub. Returns the method names it removed (sorted by discovery order). Non-test, non-_gen.go files only — the marker only ever lands in the user-owned handler files. A missing dir is not an error (nothing to excise).

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 FillHandlerColumns added in v0.1.45

func FillHandlerColumns(entity EntityDef) []string

FillHandlerColumns names the entity's columns declaring `forge:fill=handler` — the exact set FindUnsatisfiableColumns would have flagged but for that declaration (NOT NULL, no DB DEFAULT, forge:read-only, not the PK, not GENERATED). Used to seed the handlers_crud.go Create scaffold's op.Entity wrapper (see ensureCRUDShimFile) with a named reminder of what the handler author still has to stamp.

func GenerateAPIDeepCopy added in v0.1.45

func GenerateAPIDeepCopy(projectDir string) ([]string, error)

GenerateAPIDeepCopy regenerates zz_generated.deepcopy.go for every package under the project's api/ directory, in-process, with the controller-tools version pinned in forge's go.mod.

WHY forge OWNS THIS. forge already projects the CRDs (crd_gen.k) from these same Go types, but it never regenerated their deepcopy. So deleting a type — the normal way to retire a kind — left a deepcopy file referencing it, the package stopped compiling, and the only way out was a controller-gen binary the project had no pinned version of. One source (the Go types), both projections, one command.

THE STALE FILE IS REMOVED BEFORE LOADING, because it is exactly what makes the package fail to type-check after a type is deleted. It is restored if generation fails, so a failed run never leaves the tree worse than it found it. It returns the paths it wrote, relative to projectDir.

func GenerateAuthSetup added in v0.1.45

func GenerateAuthSetup(modulePath, projectDir string) error

GenerateAuthSetup scaffolds internal/app/auth.go ONCE — the OWNED, user-editable SetupAuth() that picks the request authenticator in CODE (default: a JWT validator built from the typed config's jwt_* fields). It replaces the retired forge-owned auth_gen.go validator codegen: authentication is a code-wiring choice (which validator) reading per-deployment values that are ordinary config fields, not a static forge.yaml block.

Scaffold-once, never overwritten after first emit (os.Stat guard) — the user owns the bytes. The generated cmd serve wiring calls app.SetupAuth and threads the returned validator into the auth interceptor.

func GenerateAuthSetupData added in v0.1.46

func GenerateAuthSetupData(projectDir string, data AuthSetupTemplateData) error

GenerateAuthSetupData is GenerateAuthSetup with the whole template input: a project in graph mode passes Graph and HasDatabase.

func GenerateBootstrapTesting

func GenerateBootstrapTesting(in BootstrapTestingGenInput) error

func GenerateCRDKCL added in v0.1.45

func GenerateCRDKCL(docs []CRDDoc, controllerGenVersion string) (string, error)

GenerateCRDKCL renders the complete deploy/kcl/lib/crd_gen.k module: one `<lower>_crd = lambda -> any { ... }` per CRD, in deterministic kind order.

The generated lambdas take no arguments and close over nothing, exactly like the hand-authored module they replace, so every call site keeps its shape.

controllerGenVersion is stamped into each CRD's controller-gen.kubebuilder.io/version annotation. It names the schema SEMANTICS the output was produced under, which is the one piece of provenance that matters when a future controller-tools bump changes how some Go construct projects.

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 an anchor file so the package compiles with zero items. The services anchor is Tier-1 (register_gen.go) because it projects an input — the built-in collision NOTEs; the workers/ and operators/ anchors are SCAFFOLD-ONCE (register.go) because they project nothing. 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 scaffold 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 project 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.

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

func GenerateCmdServerWithFields

func GenerateCmdServerWithFields(configFields map[string]bool, 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).

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 GenerateConfigKCL added in v0.1.45

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

GenerateConfigKCL emits the ONE generated config module — deploy/kcl/config_gen.k — carrying both halves of the projection: the TYPE (`schema ConfigSecretRef` + `schema AppConfig`) and the BEHAVIOR (`appConfigEnvMap`).

They were two files (config_schema.k + config_projection.k) on the premise that a KCL module cannot hold a schema and a lambda together. It can — forge's own kcl/workload.k interleaves schemas and lambdas in one file. Since both halves are projected from the same proto by the same run and are only ever valid as a pair, splitting them bought nothing and cost a second Tier-1 file, a second hash guard and a second diff entry in every change. Merging also removes the cross-module import: the lambda now names `AppConfig` directly instead of `config_schema.AppConfig`.

Layout is TYPE first, then BEHAVIOR — the lambda's signature references AppConfig, and reading the shape before the projection over it is the order that explains itself.

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

func GenerateConfigKCLPerBinary added in v0.1.45

func GenerateConfigKCLPerBinary(rootFields []ConfigField, perBinary []BinaryConfigFields, projectName string) (string, error)

GenerateConfigKCLPerBinary emits the config module for a project that declares PER-BINARY configs: one typed schema plus one env-projection lambda per binary, all in the single generated config_gen.k.

Each pair is projected from ONLY that binary's fields, which is what makes the deploy story disjoint: an env authoring `AdminConfig { ... }` can set only fields admin reads (anything else is a KCL type error), and the workload env built from adminConfigEnvMap carries only admin's env vars — so gateway's Deployment does not carry admin's credentials.

rootFields is the project-global AppConfig's field set. It is still emitted when non-empty, because a project mid-migration (or one that keeps a shared AppConfig alongside per-binary ones) has env files and main.k modules that reference AppConfig/appConfigEnvMap, and those are user-owned scaffold-once files forge does not rewrite.

func GenerateConfigKScaffold

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

GenerateConfigKScaffold emits deploy/kcl/<envName>/config.k — the per-env, user-owned typed AppConfig VALUES instance — 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 GenerateConfigKScaffoldPerBinary added in v0.1.45

func GenerateConfigKScaffoldPerBinary(rootFields []ConfigField, perBinary []BinaryConfigFields, projectName, kclDirAbs, envName string) (bool, error)

GenerateConfigKScaffoldPerBinary emits the per-env, user-owned config.k for a project with PER-BINARY configs: one typed instance per binary (`admin_config: config_gen.AdminConfig = {...}`), each carrying only the values that binary reads.

rootFields is the project-global AppConfig's field set, still emitted as `app_config` when non-empty so a project keeping a shared AppConfig alongside per-binary ones has both. Write-if-absent, exactly like the single-config scaffolder: forge writes it once and never clobbers edits.

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 project 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 project-level, forge-owned KCL module that backs the config path — <kclDirAbs>/config_gen.k — from the proto-derived config fields, and withdraws the two files it replaced.

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 GenerateConfigNativeSharedPerBinary added in v0.1.45

func GenerateConfigNativeSharedPerBinary(rootFields []ConfigField, perBinary []BinaryConfigFields, projectName, projectDir, kclDirAbs string, cs *checksums.FileChecksums) error

GenerateConfigNativeSharedPerBinary is the per-binary form of GenerateConfigNativeShared: it emits the project-level, forge-owned config_gen.k carrying one typed schema + one env-projection lambda PER BINARY, so each workload's env is built from only its own binary's fields.

rootFields is the project-global AppConfig's field set, still emitted when non-empty (a project may keep a shared AppConfig alongside per-binary ones, and mid-migration every existing env file still names it).

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 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 GenerateEntityFactories added in v0.1.45

func GenerateEntityFactories(projectDir, modulePath string, services []ServiceDef, cs *checksums.FileChecksums) error

GenerateEntityFactories writes one forge-owned factories_gen_test.go per handler package, holding the typed New<Entity> row factories for the entities that package's CRUD RPCs own, and the New<CreateRequest> request factories for its create RPCs (create_request_factory.go).

Forge-owned + checksum-tracked, and regenerated on every run from the APPLIED schema. A project with no schema to model writes nothing. A schema that cannot be READ (an unreachable shadow server, a migration that does not apply) also writes nothing — the file on disk is left as it was rather than rewritten from no information — and only an embedded-postgres fetch failure is returned, so the caller can say why. A factory forge cannot derive a valid row or request for is still EMITTED, with a body that fails the calling test naming postgres's reason, and that reason is printed here as a warning: the scaffold-once lifecycle test calls these by name, so leaving one out would break compilation of every test beside it.

An entity whose CRUD RPCs no service declares is SKIPPED rather than parked in a shared package. The factory's whole value is being callable from the test that exercises that entity's handlers, and Go's in-package-_test.go visibility rule means it can only be called from the directory it lands in — so an entity with no owning handler package has no directory that could use it. Emitting one anyway would reintroduce exactly the importable-from- anywhere non-test package this move removed.

func GenerateEnvSecretsScaffold added in v0.1.45

func GenerateEnvSecretsScaffold(fields []ConfigField, projectName, projectDir, envName string) (bool, error)

GenerateEnvSecretsScaffold emits the gitignored `secrets/<envName>.yaml` that backs the env's `file` secret provider — ONLY for a LOCAL env, and ONLY when the file does not already exist. Returns true when a fresh file was written.

One key per SENSITIVE config field, keyed by env-var NAME, every one seeded EMPTY as a labelled slot to fill. That includes DATABASE_URL: the env's KCL declares the dev connection string from the port it resolves per render, so a copy here could only go stale — see devSecretValue for the full rationale. `forge env up` stays turnkey because the KCL declaration supplies the value.

Only local envs get one. A cloud env declares `forge.ExternalSecrets {}` — forge never sees those values, so scaffolding a store there would create a second, unread place to put a production credential.

func GenerateFrontendConfigJS added in v0.1.45

func GenerateFrontendConfigJS(frontend, env string, valuesJSON string) string

GenerateFrontendConfigJS renders the runtime config document for ONE environment — the artifact KCL's projection feeds and the browser loads.

It is a classic script (not a module) assigning a global, so a blocking <script src> in the document head guarantees the value exists before any bundle code runs. values is the rendered projection: env_var name -> value.

func GenerateFrontendConfigKCL added in v0.1.45

func GenerateFrontendConfigKCL(configs []FrontendConfig, projectName string) (string, error)

GenerateFrontendConfigKCL emits the frontend config module: one typed schema + one runtime projection lambda per frontend.

The projection returns a plain dict — the exact JSON object the browser receives as window.__FORGE_CONFIG__ — keyed by the field's env_var name where it declares one, falling back to the proto field name. Using the env_var as the key keeps ONE spelling of each fact across the proto, the KCL and the generated TypeScript, so a rename cannot half-land.

Sensitive fields never appear here: ValidateFrontendConfigs refuses them before generation reaches this point, which is why (unlike the backend schema) there is no ConfigSecretRef in the emitted module.

func GenerateFrontendConfigNative added in v0.1.45

func GenerateFrontendConfigNative(configs []FrontendConfig, projectName, projectDir, kclDirAbs string, cs *checksums.FileChecksums) error

GenerateFrontendConfigNative emits the project-level, forge-owned frontend config module at <kclDirAbs>/frontend_config_gen.k.

It mirrors GenerateConfigNativeShared's ownership handling so both KCL config modules land under the same tier and the same checksum ledger. When no frontend is annotated, any previously-emitted module is removed rather than left behind stale.

func GenerateFrontendConfigTS added in v0.1.45

func GenerateFrontendConfigTS(fc FrontendConfig, projectName, platform string) (string, error)

GenerateFrontendConfigTS emits the typed TypeScript module a frontend imports INSTEAD of touching process.env / import.meta.env.

── Why a runtime document rather than build-time inlining ──────────────

Next.js and Vite both inline NEXT_PUBLIC_* / VITE_* at BUILD time: the value becomes a string literal in the bundle. That makes a built artifact environment-SPECIFIC — it carries staging's issuer forever — which is exactly what `forge env deploy` exists to avoid. Promoting a bundle built against staging into production would point real users at the staging IdP.

So the values arrive at RUNTIME instead, as a small JS document (config.js) that assigns a global before any bundle code runs. The document is a static asset: KCL renders one per environment, and promoting an artifact means shipping the same bundle beside a different config.js. Nothing is rebuilt.

A blocking classic <script src> is what makes this safe to depend on at module scope. The alternative — fetch("/config.json") at boot — is asynchronous, so every consumer would have to await it, and the OIDC provider needs its issuer during module initialization to decide whether auth is even configured. A synchronous global removes that ordering problem entirely.

── Server rendering ────────────────────────────────────────────────────

A Next.js `standalone` build also renders on the server, where there is no window. There the module reads process.env, which in a deployed workload is populated from the SAME KCL config — so both paths have one source and cannot disagree. This is not a dotenv fallback: no .env file is read, written, or consulted.

func GenerateGraph added in v0.1.46

func GenerateGraph(in GraphGenInput) error

GenerateGraph emits the composition graph for a project in graph mode (GraphComposition): compose.go reconciled, graph_gen.go and routes_gen.go regenerated.

func GenerateGraphProviders added in v0.1.46

func GenerateGraphProviders(modulePath, databaseDriver, projectDir string) error

GenerateGraphProviders scaffolds a graph-mode project's owned internal/app/providers.go (the default providers: the database pool and ORM, the outbound HTTP client, the user audience's authenticator, chain and API listener, pprof) and auth.go, each exactly once.

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 project 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 GenerateLoginBroker added in v0.1.45

func GenerateLoginBroker(modulePath, projectName, projectDir string) error

GenerateLoginBroker scaffolds internal/app/login_broker.go ONCE — the OWNED server half of the API-only sign-in flow, which lets an app render its own sign-in screen instead of redirecting the user to the identity provider's login pages.

WHY IT IS SCAFFOLDED UNCONDITIONALLY rather than behind a config check. Whether a project uses this flow is decided by config at RUN time (idp_login_uri), and config is per-environment: the same build serves an environment that uses the issuer's pages and one that does not. Emitting the file only when some environment happens to set the field would make the code's existence depend on which env file was read at generate time, which is exactly the kind of spooky action this generator avoids.

The file costs nothing when unused — nothing calls RegisterLoginBroker unless the project wires it — and a project that will never use it deletes the file, which the scaffold-once guard respects permanently.

Scaffold-once, never overwritten after first emit: the user owns the bytes, including the security-critical handler that decides which credentials get checked. See the template's header for what must not be changed carelessly.

func GenerateMigrate

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

GenerateMigrate writes db/embed.go — the project's embedded migration set — and retires the pkg/app/migrate.go that used to sit beside it.

WHAT MOVED, AND WHY. pkg/app/migrate.go was 86 lines of Tier-1 golang-migrate ceremony: read the embedded dir, build a source driver, bind the pool, read the version, refuse a dirty schema, fold ErrNoChange. Not one line of it was derived from anything the user wrote — the ONLY project-specific value in the whole file was the identifier `forgedb.MigrationsFS`. A file that is byte-identical in every project except one symbol is library code parked in the user's tree: regenerating it cannot keep it correct, because it was never a function of the inputs.

So the ceremony is now migratekit.AutoMigrate(fs, dir), and the project keeps only the call site — a single expression in the serve.go it already owns, naming its own embedded FS. Improvements to the migration logic now arrive through a forge/pkg bump instead of a re-render.

db/embed.go STAYS generated, and is the reason this function still exists: the `//go:embed migrations/*.sql` directive can only live in the db/ package beside the SQL, and whether it can be written at all is a fact re-read from disk on every run (the directive does not compile against an empty match). That is a genuine projection of user input, which is exactly what Tier-1 is for.

cs is the project's checksum tracker — db/embed.go is recorded so `forge project audit` doesn't flag drift on it. A nil cs is tolerated.

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 plus one handler file per custom RPC (RPCHandlerFileName) for a new service, using the embedded FS templates. crudMethodNames lists methods that CRUD gen will implement; these are excluded from the initial stubs.

func GenerateTierKCL added in v0.1.45

func GenerateTierKCL(in *TierSchemas, pkgPath string) (string, error)

GenerateTierKCL renders the KCL module text. It is pure: the same schemas always produce byte-identical output.

func GeneratedMarkerExpr added in v0.1.45

func GeneratedMarkerExpr(comment string) (expr string, found bool)

GeneratedMarkerExpr returns the SQL expression carried by a `forge:generated` marker in comment, trimmed, and whether comment carries the marker at all. A marker with nothing after it reports ("", true): the caller refuses that, since a GENERATED column with no expression is not a column postgres can create.

The expression is the REST OF THE LINE, verbatim. Prose cannot share the line — there is no telling where SQL ends — so a comment explaining the derivation goes on its own line above.

func GetModulePath

func GetModulePath(dir string) (string, error)

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

func GoPackageForProtoFile added in v0.1.45

func GoPackageForProtoFile(protoFile, modulePath string) string

GoPackageForProtoFile derives the generated Go import path for a proto file under the buf module root. The buf module root is "proto/", and the go_package convention forge projects use is `<module>/gen/<dir>` where <dir> is the proto file's directory relative to "proto/" (e.g. "reliant/v1/daemon_registry.proto" → "<module>/gen/reliant/v1"). Returns "" when modulePath is empty or the path has no directory component.

func GraphComposition added in v0.1.46

func GraphComposition(projectDir string) bool

GraphComposition reports whether the project composes with the composition graph: true for a new project and for one whose internal/app has no legacy composition (`type Infra struct` or `func NewComponents`) in an owned file.

func GuardTargets added in v0.1.45

func GuardTargets(comment string) []string

GuardTargets returns every `<table>.<column>` a comment declares with ProtoMarkerGuards, in declaration order. Trailing prose after the target is tolerated (`forge:guards invoices.amount_paid_cents — refuses overpayment`), a bare `forge:guards` with no target yields nothing, and a longer token that merely starts with the marker (`forge:guardsmen`) is refused — the same exactness the rest of the registry enforces, for the same reason: a marker that half-matches changes generated output on a spelling its author never wrote.

A field may carry several, which is the shape a multi-column guard actually takes: ScheduleJob owns crew_id, scheduled_start AND scheduled_end together, and declaring them one per line keeps each target individually greppable.

func HasConfigProtos added in v0.1.45

func HasConfigProtos(projectDir string) bool

HasConfigProtos reports whether proto/config holds a .proto file — the input the config loader generates pkg/config from. A proto/config directory with no proto in it (a CLI scaffold leaves an empty proto/config/v1) is not one: generating the loader from forge's default fields there writes a pkg/config importing gen/config/v1, which nothing will ever generate.

func HasConstructorMarker added in v0.1.45

func HasConstructorMarker(dir string) bool

HasConstructorMarker reports whether the package rooted at dir carries the `// forge:constructor` marker on its `func New` doc comment — the opt-in signal for the generated observability decorator.

func HasConstructorMarkerOn added in v0.1.45

func HasConstructorMarkerOn(fn *ast.FuncDecl) bool

HasConstructorMarkerOn reports whether THIS declaration carries `// forge:constructor`, as opposed to the package containing one somewhere (HasConstructorMarker).

The per-decl answer is what tells a lint whether the marker is advice the author has already taken. The marker frees a constructor's NAME and has never affected its signature, so prescribing it for a signature gap on a func that already carries it is advice that cannot work — and an author who tries it concludes the rule is unsatisfiable.

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, in either the reasoned form (`//forge:exclude-contract: <why>`) or the bare one. A package carrying this directive opts OUT of contract codegen — the per-package equivalent of forge.yaml `contracts.exclude`.

The bare form still EXCLUDES here, deliberately: codegen must never break a project over the spelling of an opt-out. It is `forge lint` that refuses a bare marker (see internal/linter/contract/exclude_directive.go), because a reason is what turns the cheapest way to silence the contract rules into a decision a reviewer can evaluate.

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 HasNotAComponentDirective added in v0.1.45

func HasNotAComponentDirective(dir string) bool

HasNotAComponentDirective reports whether the package rooted at dir declares `//forge:not-a-component` in any of its non-test .go files, with or without a reason. Such a package keeps its contract codegen (mock, and the decorator if it opts in) but is not a component of the server composition: compose.go never constructs it and never resolves its Deps.

The bare form counts here for the same reason a bare exclude-contract does: codegen must never break a project over the spelling of a marker. `forge lint` refuses the bare form (forge-not-a-component-reason).

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 HasOutboundIODirective added in v0.1.45

func HasOutboundIODirective(dir string) bool

HasOutboundIODirective reports whether the package rooted at dir declares `//forge:outbound-io` in any of its non-test .go files — the marker the adapter scaffold stamps on contract.go. It asserts one property: this package calls OUT to a third-party system and nothing calls in. Two mechanisms read it — the outbound-io-no-rpc contract check (an inbound handler contradicts the claim) and the enforce-component-observe lint's I/O heuristic (an outbound boundary does I/O by definition, so a component depending on one touches I/O).

func HasPackageNoObserveDirective added in v0.1.45

func HasPackageNoObserveDirective(dir string) bool

HasPackageNoObserveDirective reports whether the package rooted at dir opts OUT of observability at the PACKAGE level via `// forge:no-observe` (on the constructor, package doc, or contract interface). Method-level opt-outs are not reported here — see MethodDef.SkipObserve.

func HasServiceContractMarker added in v0.1.45

func HasServiceContractMarker(groups ...*ast.CommentGroup) bool

HasServiceContractMarker reports whether any of the given comment groups carries the `//forge:service` or `//forge:contract` marker. The marker must be the WHOLE comment line (after stripping comment syntax + whitespace), matching every other forge directive, so prose that merely mentions it is never a false match. Both spaced (`// forge:service`) and unspaced (`//forge:service`) forms are recognized. Shared by DetectServiceInterfaceName (codegen) and the contract-name lint (contractcheck) so the two never drift on what counts as marked.

func HostedRefusal added in v0.1.45

func HostedRefusal(kind string) string

HostedRefusal is why the hosted runtime's Restricted profile refuses a workload of kind, or "" when it admits it. The control plane is the authority (deploytarget.PreflightHosted runs its admission at render); this names the kinds a SCAFFOLD must not bind there, so a new workload is never bound somewhere it cannot run.

func InstanceName added in v0.1.45

func InstanceName(messageName string) string

InstanceName is the member a config's instance takes on the bundle, and the name the per-env config.k scaffold declares it under: `ApiServerConfig` -> `api_server_config`.

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) reads a project's services through.

It enumerates SERVICES only. The supervised kinds (worker, operator) and secondary binaries have no proto contract, so they are discovered from their package directories instead — see DiscoverProjectComponents, which unions the two into an Inventory and is what every caller that needs the project's FULL component set goes through.

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 IsComponentConstructor added in v0.1.45

func IsComponentConstructor(fn *ast.FuncDecl) bool

IsComponentConstructor reports whether fn is a constructor forge should treat as a component entry point.

The rule, in priority order:

  1. a func carrying `// forge:constructor` IS one, whatever it is named;
  2. otherwise a func named `New` is one, so an unmarked package still works.

WHY THIS EXISTS. Three separate scanners used to spell this as `fn.Name.Name != "New"`, which made `// forge:constructor` unreadable on any other name — the marker whose entire job is to identify a constructor was gated on the constructor already being called `New`. Tagging `NewReadOnly`, `Open` or `Connect` did nothing, silently.

It also forced a name. forge detects the CONTRACT name (DetectServiceInterfaceName, with `Service` only as a fallback) precisely so a package can call its interface `Mailer` or `Repository`; the constructor deserves the same freedom. forge is meant to promote good practice, not to require that every component be `New` returning `Service`.

func IsKnownProtoMarker added in v0.1.45

func IsKnownProtoMarker(name string) bool

IsKnownProtoMarker reports whether name is a marker forge reads out of a proto comment. The comparison is EXACT: `forge:read-only-ish` is not `forge:read-only`, the same discipline the column-marker check enforces so a longer marker cannot pass as a shorter one it merely starts with.

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 IsProtoScalarKind added in v0.1.45

func IsProtoScalarKind(kind string) bool

IsProtoScalarKind reports whether the token names one of the fifteen proto scalar kinds.

It reads the closed table rather than carrying a set of its own, so the scanner's notion of "this token is a scalar, not a type name" and the projections' notion of "I have an answer for this kind" are the same notion. When they were two, a kind either table omitted was classified as a message type name by one and projected to `string` by the other, and neither said anything.

func IsScaffoldTestFile added in v0.1.45

func IsScaffoldTestFile(name string) bool

IsScaffoldTestFile reports whether a bare filename is one of forge's born per-RPC scaffold tests. It also accepts the single-file `handlers_scaffold_test.go` that projects born before the per-RPC split still have: that file is on real users' disks and the consumers of this predicate (stale-pb-reference detection, test-helper reconciliation) have to keep working on it.

func IsUnmanagedMarkerLine added in v0.1.45

func IsUnmanagedMarkerLine(line string) bool

IsUnmanagedMarkerLine reports whether one raw proto source line is the `// forge:unmanaged` marker, by the same rule the raw scan applies — for line scanners outside this package that track their own declarations.

func IsWiredComponentDir added in v0.1.45

func IsWiredComponentDir(dir string) bool

IsWiredComponentDir reports whether dir is a forge-WIRED component — the single predicate that decides whether a package occupies a slot in the generated test-factory namespace (`app.NewTest<X>`).

A wired component is a package forge itself constructs, which means all four of:

  • it declares a contract.go (the Service/Deps/New triple lives there);
  • it is not opted out with `//forge:exclude-contract` — a types-only package (internal/db, internal/deploy) carries this precisely because there is nothing to construct;
  • it is not `//forge:external-component` — hand-built in providers.go / OpenInfra, so forge emits no factory for it either;
  • it is not `//forge:not-a-component` — the server composition does not construct it at all (a CLI- or daemon-only package), so there is no forge-wired instance for a factory to stand in for. It keeps its mock_gen.go, which is the seam its consumers test against.

WHY THIS EXISTS AS ONE FUNCTION. The name is derived by two generators — the helpers generator (which builds its namespace from real components) and ComputeTestHelperName (which drives the scaffold tests). They used to ask DIFFERENT questions: the helpers generator asked "is this a component", ComputeTestHelperName asked only `os.Stat(internal/<pkg>)`, "does a directory exist". A types-only internal/deploy answered no to the first and yes to the second, so testing.go declared NewTestDeploy while sixteen scaffold tests referenced NewTestSvcDeploy, and the package did not compile. Routing both through this predicate is what makes that disagreement unrepresentable rather than merely fixed once.

The forge.yaml `contracts.exclude` list is deliberately NOT consulted here: it is a whole-subtree opt-out read by DiscoverInternalPackages, which removes those packages from the inventory before either generator sees them. A path that never reaches the namespace cannot collide with it.

func KCLAllSensitiveName added in v0.1.45

func KCLAllSensitiveName(schemaName string) string

KCLAllSensitiveName maps a config SCHEMA name to the generated constant listing every sensitive ENV_VAR that schema declares: `AppConfig` -> `APP_CONFIG_SENSITIVE_ENV`.

It exists so the one caller that legitimately wants EVERY credential — forge's own host-mode config probe, which resolves what the env's config evaluates to for `forge env up`, the parity report and the seed gate — can say so by name instead of hand-listing vars that would go stale the moment a field is added. A cluster WORKLOAD must never use it: naming the whole set is precisely the broadcast this design removed.

func KCLConfigName added in v0.1.45

func KCLConfigName(messageName string) (schema, lambda string)

KCLConfigName maps a config MESSAGE name to the KCL schema and lambda it projects to: `AdminConfig` -> schema `AdminConfig`, lambda `adminConfigEnvMap`. The lambda name is the schema name lower-camelled with the `EnvMap` suffix, matching the existing appConfigEnvMap spelling so the per-binary names read as the same family.

func KCLFrontendConfigName added in v0.1.45

func KCLFrontendConfigName(messageName string) (schema, lambda string)

KCLFrontendConfigName maps a frontend-bound config MESSAGE name to the KCL schema and projection lambda it generates: `WebConfig` -> schema `WebConfig`, lambda `webConfigRuntime`. The `Runtime` suffix (rather than the backend's `EnvMap`) says what the projection produces: the runtime config document the bundle reads, not a set of env vars.

func KCLOptionalSecretsName added in v0.1.45

func KCLOptionalSecretsName(schemaName string) string

KCLOptionalSecretsName maps a config SCHEMA name to the generated constant listing every sensitive ENV_VAR that schema marks `optional: true`: `AppConfig` -> `APP_CONFIG_OPTIONAL_SECRET_ENV`.

It is the seam the secret PRE-FLIGHT reads. `forge env up` validates every declared secret_ref against the store before anything starts, and it works from the rendered entities — it has no proto and no descriptor in hand. So the proto's `optional` annotation is projected to KCL here and travels with the render, which keeps the dependency running one way: the secrets package never learns what a config message is.

Emitted even when empty so a reference resolves in a project that marks nothing optional.

func LegacyConfigModules added in v0.1.45

func LegacyConfigModules() []string

LegacyConfigModules names the pre-merge KCL modules that config_gen.k replaced: the TYPE half (config_schema.k) and the BEHAVIOR half (config_projection.k), which were two Tier-1 files, two hash guards and two diff entries for one projection. Existing projects have them on disk; RemoveLegacyConfigModules withdraws forge's own copies once the merged file is in place.

func MigrateArgs added in v0.1.45

func MigrateArgs() []string

MigrateArgs returns the subcommand of the project binary that applies its embedded migrations (`db migrate up`, cmd-tree-db.go.tmpl).

It is the workload's `args`, not a container argv: the image's ENTRYPOINT is the binary (Dockerfile.tmpl), and the host runtime derives `go run ./cmd/<project> db migrate up` from the same build + args. One declaration selects the subcommand on every runtime, so it cannot drift from the image or the host process it runs in.

This is a SCAFFOLD-TIME default only. It is written literally into deploy/kcl/workloads.k once, and the project owns it from then on: how a system migrates is an operational decision that differs per environment and changes over time (an env may migrate out of band, or run a different tool entirely). forge does not re-derive it.

func MigrateConfigBlockReferences added in v0.1.45

func MigrateConfigBlockReferences(kclDirAbs string, instances []ConfigInstanceBlocks) ([]string, error)

MigrateConfigBlockReferences rewrites flat block-leaf references in each env's config.k and main.k under kclDirAbs to their nested paths, for the given instances. Returns the project-relative files it rewrote.

func MigrateWorkloadStanza added in v0.1.45

func MigrateWorkloadStanza(modulePath, projectName string) string

MigrateWorkloadStanza renders the deploy-time migration step as the KCL literal that belongs in deploy/kcl/workloads.k.

It is an ORDINARY WORKLOAD — `kind = "job"` with the broadcast `before` — and that is the entire point. Migration used to be a bespoke `migrate: [str]` field on WorkloadEnv that the render layer had special knowledge of. It is now one instance of the one-shot primitive, so it reads, refines, and can be dropped exactly like anything else the project ships.

`before = [fw.BEFORE_ALL]` rather than an enumerated list of dependents is the load-bearing choice. An enumerated list is a list that goes STALE: add a workload six months from now and the list still renders, still passes every check, and silently does not gate the new one — a pod serving traffic against a schema it does not have. The broadcast selector has nothing to forget to update.

The `build` names the project's own binary, the same GoBuild every component declares: forge builds a (cmd, output) pair once however many workloads name it, so this compiles nothing extra. It is what lets the host runtime derive `go run ./cmd/<project> db migrate up`, and the cluster runtime pick the image, from one declaration.

`config_secrets = ["DATABASE_URL"]`: the job needs the DSN and nothing else, so it is the one credential projected into it.

func MockRelPath added in v0.1.45

func MockRelPath(svc ServiceDef) string

MockRelPath is the project-relative path of svc's generated mock — the one name both GenerateMock's write and generate's progress line use, so the log can never name a file the run did not write (it said <svc>_mock.go after the file became <svc>_mock_gen.go).

func OptionalSecretEnvVars added in v0.1.45

func OptionalSecretEnvVars(messages []ConfigMessage) []string

OptionalSecretEnvVars returns the env-var names of every sensitive field marked optional, across all messages.

This is the seam the secret pre-flight consumes: it works in terms of env names (that is what a secret_ref carries by the time it reaches the store), so the proto-level annotation is reduced to a name set here rather than teaching the secrets package about config messages. The dependency runs one way — secrets knows nothing about proto.

Non-sensitive optional fields are deliberately NOT included. `optional` on an ordinary field already means "no value needed" everywhere it matters, and nothing pre-flights it; returning it here would only invite a caller to think this set means something broader than it does.

func OwnerColumn added in v0.1.45

func OwnerColumn(entity EntityDef) string

OwnerColumn returns the entity's `forge:owner` column name, or "" when the table declares none. See CRUDMethodTemplateData.OwnerColumn.

func ParseCRUDOperation

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

ParseCRUDOperation extracts the CRUD operation and entity name from a method NAME alone. Returns ("", "") if the name doesn't match a CRUD pattern. Name-only, so it cannot see `// forge:custom` or streaming: classify an rpc you hold as a Method with CRUDShape instead.

func ParseExcludeContractComment added in v0.1.45

func ParseExcludeContractComment(raw string) (reason string, ok bool)

ParseExcludeContractComment reports whether one raw comment IS the exclude-contract directive, and its reason. Accepted, spaced or not:

//forge:exclude-contract: <why>   → ok, reason "<why>"
//forge:exclude-contract          → ok, reason "" (bare — lint refuses it)

Anything else — prose that mentions the directive, `forge:exclude-contract, forge:outbound-io, …` lists, a different separator — is not the directive, matching every other forge marker's whole-line rule.

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 ParseNotAComponentComment added in v0.1.45

func ParseNotAComponentComment(raw string) (reason string, ok bool)

ParseNotAComponentComment reports whether one raw comment IS the not-a-component directive, and its reason — the same two accepted spellings as ParseExcludeContractComment.

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 PendingServiceMount added in v0.1.45

func PendingServiceMount(root, bin, runtimeName string) string

PendingServiceMount reports the `(*Components).Mount<Svc>` method the service's generated mount file is waiting for — non-empty exactly when that file is the MountMissing placeholder (see cmd-svc-mount-gen.go.tmpl). It reads the file generate just wrote rather than re-deriving the answer, so the scaffold's report can never disagree with what was emitted.

func ProjectBinaries added in v0.1.45

func ProjectBinaries(projectDir string) []string

ProjectBinaries lists the binaries a (forge.v1.binary_config).binary may name: the directory leaves under projectDir/cmd.

A project with no cmd/ directory has NO binaries (an empty, non-nil slice), so a binding there is refused. nil means UNKNOWN — cmd/ exists but could not be read — and the existence check is then skipped rather than reporting every binding as dangling: "forge could not tell" must never render as "it does not exist".

func ProjectBinaryRuns added in v0.1.45

func ProjectBinaryRuns(projectName, buildCmd string) bool

ProjectBinaryRuns reports whether a workload declared with build command buildCmd runs the project's primary binary — the `./cmd/<project>` GoBuild every scaffolded stanza names (projectGoBuild).

func ProjectHasSQLMigrations added in v0.1.45

func ProjectHasSQLMigrations(projectDir string) bool

ProjectHasSQLMigrations reports whether db/migrations/ contains at least one .sql file — the ONE predicate every consumer of "does this project ship migrations" reads, so they cannot disagree:

  • GenerateMigrate embeds db/embed.go's MigrationsFS
  • component_test_helpers.go.tmpl imports forgedb (which only exists when GenerateMigrate saw migrations)
  • cmd-tree-db.go.tmpl compiles `db migrate` against the EMBEDDED set
  • the scaffolded per-env `migrate` argv, which the env render turns into a migration initContainer

func ProjectHasTypedConfig added in v0.1.45

func ProjectHasTypedConfig(projectDir string) bool

ProjectHasTypedConfig reports whether the project has the typed config a scaffolded component's `Deps.Config *config.Config` names: config protos the loader turns into pkg/config, or a pkg/config that already declares Config (a CLI's stub, a hand-written one).

A project adopted in place often has neither — its configuration lives in its own package. A component scaffolded there takes no Config: importing <module>/pkg/config would not compile, and a placeholder package configuring nothing is an empty default every caller would have to construct and pass. The project adds the field when it has config to give.

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 ProtoMarkerAnyCommentRE added in v0.1.45

func ProtoMarkerAnyCommentRE(markers []string) *regexp.Regexp

ProtoMarkerAnyCommentRE is ProtoMarkerAnyLineRE for comment text buf has already stripped the `//` from — the descriptor path's shape.

func ProtoMarkerAnyLineRE added in v0.1.45

func ProtoMarkerAnyLineRE(markers []string) *regexp.Regexp

ProtoMarkerAnyLineRE builds a recognizer matching ANY of markers in a `//`-prefixed proto comment, with the same spacing/trailing-prose rules as the single-marker builder. Alternation is over quoted marker names, so a longer token that merely starts with one of them is still refused.

func ProtoMarkerCommentRE added in v0.1.45

func ProtoMarkerCommentRE(marker string) *regexp.Regexp

ProtoMarkerCommentRE builds the recognizer for a marker read out of a comment buf has ALREADY stripped the `//` from — the shape protogen hands to the descriptor path in internal/cli/forge_descriptor.go. Spacing and trailing prose are tolerated as above; `(?m)` lets it match any line of a multi-line comment block.

Exported because its callers live outside this package, unlike the two raw-source builders above.

func ProtoScalarGoType added in v0.1.45

func ProtoScalarGoType(kind string) (string, bool)

ProtoScalarGoType returns the Go type protoc-gen-go generates for a proto SCALAR kind. ok=false for anything else ("message", "enum", "map", a well-known type name) — callers branch on those before asking, and an unknown kind must never silently answer `string`, which is indistinguishable from the correct answer for the one kind that really is a string.

func ProtoScalarKinds added in v0.1.45

func ProtoScalarKinds() []string

ProtoScalarKinds returns the closed proto scalar vocabulary, sorted — the exact key set of the table above, never a restatement of it.

It is exported for the end-to-end corpus, which has to answer a question no unit test can: which kinds actually travelled scaffold → generate → proto → TypeScript → tsc. That corpus previously carried a hand-written list of field types, which is a guard that cannot fail — it covers the kinds someone remembered, and `bytes` was not one of them, so a `bytes` column shipped green and produced 33 tsc errors in a generated frontend. Deriving the corpus's obligation from this table instead means a kind added here is either exercised end-to-end or fails a test by name.

func ProtoTypeToGoType

func ProtoTypeToGoType(protoType string) string

ProtoTypeToGoType converts a proto SCALAR kind to the Go type protoc-gen-go generates for it.

This is not forge's choice to make: the generated `pb` package already declares the field, and every projection that disagrees with it produces code that either refuses a legal shape or does not compile. Nine of the fifteen kinds used to answer "string" here — every unsigned, zigzag and fixed-width integer, plus `bytes` — with two different consequences:

  • `bytes` and the eight integer kinds were rejected by the CRUD conversion gate, so an app declaring one could not run `forge generate` at all;
  • `repeated bytes` PASSED, because the wire side answered []string and the column side (dbBaseGoType, collapsing every non-BIGINT[] array) also answered []string. Two wrong answers agreeing looked exactly like a mapped pairing, and the generator emitted `append([]string(nil), m.Chunks...)` against protoc-gen-go's real [][]byte field.

A non-scalar kind ("message", "enum", "map", "group") has no scalar Go type; callers branch on those BEFORE reaching here (see schemaFieldToEntityField, classifyFilterField), and the "string" answer is the inert fallback for a kind that never should have arrived. It is spelled here, at the one site that needs it, rather than as a `default:` arm on the table: a fallback inside the projection is reached by an unrecognised kind too, and cannot be told apart from a mapping.

func RPCHandlerFileName added in v0.1.45

func RPCHandlerFileName(rpc string) string

RPCHandlerFileName is the born handler file for one custom RPC — `rpc_<snake_name>.go`.

Same rule, same reason, as ScaffoldTestFileName next door: the name is derived from the RPC alone, so the mapping is total and reversible without a manifest, and two authors implementing two RPCs of the same service have nothing to merge. `forge scaffold rpc` has always written this name for an RPC that is not in the proto yet (internal/cli/scaffold/rpc.go); the generate pipeline now writes it too, so the two paths agree instead of disagreeing by which order the user happened to do things in.

There is no legacy-name predicate to go with it (no RPCHandlerFile analogue of IsScaffoldTestFile) because nothing needs one: every reader of these files — ScanExistingMethods, ScanUnwiredStubMethods, ExciseUnwiredStubs, `forge project audit`, mock_gen — walks the handler directory and is already indifferent to filenames. Projects born before the split keep their handlers.go and keep working, untouched.

func ReadOnlyColumns added in v0.1.45

func ReadOnlyColumns(entity EntityDef) []string

ReadOnlyColumns returns, in column order, every column of entity whose wire field is marked `forge:read-only` or `forge:computed` (EntityField .ReadOnly covers both — see ReadOnlyProtoMarkers). The primary key is left out: it addresses the row rather than being written by it.

A read-only wire field with no column is not a column anyone can write, and a column with no wire field is not one a client can name, so both are outside the answer.

func ReconcileConfigModuleImports added in v0.1.45

func ReconcileConfigModuleImports(kclDirAbs string) ([]string, error)

ReconcileConfigModuleImports rewrites references to the superseded config_schema / config_projection KCL modules to the merged config_gen across the USER-OWNED files that name them: each env's config.k (which does `import config_schema` and types its instance `config_schema.AppConfig`) and each env's main.k (`import config_projection`, `config_projection.appConfigEnvMap(...)`).

Rewriting user-owned files needs justifying, since forge's rule is that it does not touch them. The alternative is worse: the module those files import no longer exists, so a project that regenerates without this fix gets a KCL tree that does not load at all — `forge env up`, `forge env deploy` and every render fail on an unresolved import until the user hand-edits every env. This is the same reconciliation ReconcileScaffoldTestHelperName performs when a forge-owned identifier a scaffold references gets renamed: forge renamed the symbol, so forge fixes the references it caused to dangle.

The rewrite is deliberately narrow — the module NAME only, on the import statement and on qualified references. Values, comments the user wrote, and every other line are preserved byte-for-byte. Returns the project-relative paths it rewrote, for the caller to report.

func ReconcileScaffoldTestHelperName added in v0.1.45

func ReconcileScaffoldTestHelperName(projectDir, servicePkg, targetDir string) (bool, error)

ReconcileScaffoldTestHelperName re-points the born scaffold tests' references to the forge-generated `app.NewTest<X>` / `app.With<X>Deps` factories at the collision-aware helper name pkg/app/testing.go actually emits for this service, and reports whether it rewrote anything.

It sweeps every scaffold test in the directory (IsScaffoldTestFile), not one fixed filename: the scaffold is one file per RPC.

GenerateServiceStub writes the scaffold test ONCE, at `forge project new` / `forge scaffold service`, using whatever ComputeTestHelperName resolved THEN — before any same-named domain package exists, so the plain form (`NewTestWidget`). When the RPC-vertical sweep later creates the `internal/<svc>` domain package, GenerateBootstrapTesting / ComputeTestHelperName flip the factory to the Svc-prefixed form (`NewTestSvcWidget`) to disambiguate it from the domain package's own `NewTestPkg<X>` factory — but the scaffold-once test is frozen on the stale name, an undefined reference that fails `go vet` on the very next generate. Reconciling ONLY these forge-owned factory identifiers on every generate keeps the owned scaffold test in lockstep with the regenerated testing.go, without touching the user's own test logic. It is idempotent (a no-op once the names already agree) and symmetric — it also rewrites back to the plain form if the domain package is later removed.

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 RemoveLegacyConfigModules added in v0.1.45

func RemoveLegacyConfigModules(kclDirAbs string) error

RemoveLegacyConfigModules deletes the pre-merge config_schema.k / config_projection.k once config_gen.k supersedes them.

This is the same withdrawal RemoveK3dPorts performs for a generated file forge stops emitting: the files are Tier-1 forge output that nothing imports anymore, and leaving them would be worse than deleting them — they would sit in the tree as a stale second definition of AppConfig that no longer tracks the proto, and (being in the same KCL package as the merged file) would collide on the very symbols it declares.

Only forge's OWN renders are removed: a file whose generated-by stamp is missing has been taken over by the user, and it is left in place. It is deliberately NOT gated on the checksum manifest — a project regenerated by an older binary, or one whose ledger was rebuilt, still needs the collision cleared.

func RenderOpenProcedures added in v0.1.45

func RenderOpenProcedures(services []ServiceDef, modulePath string) ([]byte, error)

RenderOpenProcedures renders pkg/middleware/procedures_gen.go.

func RenderScopedCRUDShim added in v0.1.45

func RenderScopedCRUDShim(method, inputType, outputType, entity, ownerColumn string) (string, error)

RenderScopedCRUDShim renders the owner-scoping handler wrapper for one CRUD RPC: the caller resolution, the placeholder owner expression, and the op-seam override that puts ownerColumn into the query.

It is the same template, through the same renderer, that ensureCRUDShimFile uses at a handler file's birth — so what a user pastes into an existing handlers_crud.go and what a greenfield project is scaffolded are identical.

method must be a CRUD RPC name (Create/Get/List/Update/Delete + entity) and ownerColumn must be non-empty; anything else is an error rather than a guess.

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 RetireGenerated added in v0.1.45

func RetireGenerated(projectDir, relPath string, cs *checksums.FileChecksums)

RetireGenerated deletes a generated file forge has STOPPED EMITTING outright — no successor, no rename, the output simply no longer exists.

This is the sibling of RetireRenamedGenerated, and the distinction is the carry-over. A rename has a destination, so a disowned copy is MOVED to the new name (the user's ownership survives forge's bookkeeping). A removal has no destination to carry anything to: the file is either forge's, in which case it goes, or the user's, in which case it stays exactly where it is and becomes an ordinary hand-maintained module. Routing a removal through the rename helper would invent a `_gen` twin nothing writes and move the user's file to a name they never chose.

Ownership is read from the file, never assumed — same test as every other retirement path: a verifying forge:hash marker certifies the bytes as forge's own render and it is removed; a Modified marker means hand-edited and it is kept; a disowned path is never touched. A path that does not exist is the common case (a project scaffolded after the removal) and is silently fine.

func RetireRenamedGenerated added in v0.1.45

func RetireRenamedGenerated(projectDir, oldRelPath string, cs *checksums.FileChecksums)

RetireRenamedGenerated deletes the OLD path of a generated file that forge has renamed, so the project is left with one copy under the new name rather than two that drift apart.

WHY A RENAME NEEDS THIS AT ALL. A generated file's old copy is still on disk after the emitter starts writing a new name, and it still carries forge's "Code generated — DO NOT EDIT" banner and hash marker. Nothing emits it anymore, which makes it exactly what the stale-artifact sweep deletes — but the sweep runs after every emitter, and before it the package has two declarations of the same symbols and does not compile. Reclaiming it here, in the emitter that supersedes it, is what makes a rename a single atomic-looking change to the user.

OWNERSHIP IS READ FROM THE FILE, never assumed. A copy that still verifies as forge's own render is forge's to remove; a hand-edited one is the user's work and is LEFT ALONE, even though that means the project keeps a stale file. Deleting a file forge did not write is not a tradeoff worth making to tidy a rename: the user can remove it, and forge cannot un-delete it.

A DISOWNED path is neither deleted nor ignored: it is CARRIED. The user disowned forge's output; the fact that forge later renamed that output is forge's own bookkeeping, not a user action that should void their ownership. Leaving the record on the old path was the fr-disown-rename bug: the new path looked unowned, so the emitter wrote a second file declaring the same symbols beside the user's, silently, and the package stopped compiling. CarryDisownAcrossRename moves the user's file (and its record) to the new name BEFORE the emitter writes, so the emitter's own disown check skips it and a collision cannot occur. See checksums/disown_rename.go for the full rationale and the refusal case.

Files with no marker at all fall back to the legacy banner check, which is what reaches projects generated before markers existed.

It is deliberately quiet about a path that does not exist — that is the common case (a project scaffolded after the rename) and not an event.

func RetiredEntityFactoryRelPath added in v0.1.45

func RetiredEntityFactoryRelPath() string

RetiredEntityFactoryRelPath is the pre-move location of the entity factories: their own non-test package, which every project scaffolded before this change still carries. GenerateEntityFactories' caller retires it so an upgrading project does not end up with both copies — the old one being a non-test .go file that imports `testing`, which is the shape this move exists to remove.

func RunsBuiltinSubcommand added in v0.1.45

func RunsBuiltinSubcommand(args []string) bool

RunsBuiltinSubcommand reports whether args, handed to the project binary, select a command the generated cmd tree itself owns rather than a component: `server` (every component in one process), `version`, `db …` (the scaffolded migrate job),

Judged by the FIRST arg, the top-level subcommand, so every leaf under a built-in (`db migrate status`, a later `auth` verb) counts without being listed. The set is derived, not restated: the top-level names the cmd tree reserves (reservedSubcommandNames) plus the command each scaffolded job's args select (MigrateArgs).

func RunsInServer added in v0.1.45

func RunsInServer(kind string) bool

RunsInServer reports whether a workload of kind is part of what the project binary's `server` runs: a service (mounted on its mux) or a worker (supervised beside it). Jobs run to completion on their own; an operator needs the Kubernetes API, which a hosted `server` does not have (it logs that operators are disabled and serves without them); a tool never runs.

func SameWorkload added in v0.1.46

func SameWorkload(a, b string) bool

SameWorkload reports whether two names spell the same workload: a declared `name` and a component name, in any of their forms.

func ScaffoldImageRef added in v0.1.45

func ScaffoldImageRef(modulePath, projectName string) string

ScaffoldImageRef is the image reference a scaffolded workload declares: the FULL reference, registry host included, because that is the only place a registry is declared — no environment carries one.

The registry is derived from the module path's GitHub owner (github.com/acme/shop → ghcr.io/acme/shop), which a GitHub-hosted project can push to with nothing but its workflow token.

A module path that names no GitHub owner gets a placeholder HOST rather than a bare name, and the shape of that placeholder is load-bearing:

  • It must contain a dot, so it parses as a registry host. A bare `REPLACE_ME_REGISTRY/shop` does NOT — Docker's rule is that the first segment is a host only if it has a `.` or `:` — so the render would refuse the scaffold forge itself just wrote, and `forge env new --check` could not compile a fresh project.
  • It must be obviously unreal. `.invalid` is reserved by RFC 2606 and can never resolve, so a scaffold that reaches a real push fails at the placeholder instead of silently pushing somewhere that happens to exist.

So the tree compiles, the value is visibly a placeholder, and the failure — if anyone tries to push it — names a host that provably belongs to nobody.

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 ScaffoldTestFileName added in v0.1.45

func ScaffoldTestFileName(rpc string) string

ScaffoldTestFileName is the born unit-test file for one RPC.

One RPC per file, and the name is derived from the RPC alone, so the mapping is total and reversible without a manifest: a reader seeing handlers_scaffold_ship_order_test.go knows exactly which RPC it covers, and a second author implementing a different RPC in the same package has nothing to merge.

The prefix is stable and is what every consumer matches on (IsScaffoldTestFile) — including the pre-split `handlers_scaffold_test.go` that older projects still carry on disk.

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 scaffold 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 ScanExistingMethods added in v0.1.45

func ScanExistingMethods(dir string, includeGeneratedStubs bool) (map[string]bool, error)

ScanExistingMethods reads all .go files in dir and returns a set of method names that are already implemented on *Service. It uses go/parser so that multi-line receivers, comments, and strings containing "*Service" are handled correctly.

This is the dedup that lets a user's `handlers.go` claim a method (e.g. `func (s *Service) CreateUser(...) ...`) and have the next `forge generate` automatically drop the matching stub from `handlers_gen.go`. Same shape closes the FORGE_REVIEW_PROCESS.md §2.3 git_credential drift class — gen-files and user-files share the `*Service` receiver, so a method declared in either is sufficient signal that the proto RPC is implemented.

An individual file that fails to parse is skipped with a warning rather than failing the whole pass: a transient syntax error in a sibling file must not brick the dedup for the entire package, since losing dedup means the user's just-written `CreateUser` would be re-stubbed in handlers_gen.go and the package would fail to compile (duplicate method).

Exported for internal/scaffold (the typed RPC vertical shares this hand-implemented-method detection with the stub generator).

func ScanHandlersCrudMethods added in v0.1.45

func ScanHandlersCrudMethods(dir string) map[string]bool

ScanHandlersCrudMethods returns the set of *Service method names declared in handlers_crud.go specifically. ScanExistingMethods skips that file wholesale (its delegating shims must not suppress ops regen); this lets the stub generator look inside it to find HAND-WRITTEN (non-CRUD) impls that DO need to suppress a duplicate stub. Returns an empty set if the file is absent or unparseable — losing this signal only risks a duplicate-method compile error surfacing at the validate step, never a silent wrong result.

func ScanUnwiredStubMethods added in v0.1.45

func ScanUnwiredStubMethods(dir string) map[string]bool

ScanUnwiredStubMethods returns the set of RPC method names in one handler directory that still carry the unwired-stub marker — i.e. the generated CodeUnimplemented stubs nobody has implemented yet. Non-test, non-_gen.go files only (the marker only ever lands in the user-owned handler files). Unreadable dirs return an empty set.

func SeedFingerprint added in v0.1.45

func SeedFingerprint(projectDir string, cfg seedplan.Config) (files, withConfig seedplan.Fingerprint)

SeedFingerprint identifies the schema + seed config the project's fixtures are derived FROM, for the generated guard that has to notice when those inputs move without a regenerate.

It is deliberately NOT a method on SeedProjection, and that is the whole point of its existence. A projection requires a reachable postgres to build; the fingerprint requires only files on disk. Hanging it off the projection would delete the guard in exactly the case that needs it most — no shadow server, so BuildSeedProjection returns nil, every fixture degrades to a synthetic placeholder, and the project would then also carry no record of which schema those placeholders stood in for. Reading the inputs directly means the fixtures and the claim about them fail independently. It returns BOTH digests, and the caller must keep them straight:

  • files hashes the migrations and vocab.yaml only. This is the one a process with no database can recompute — the emitted TypeScript guard reads exactly these files.
  • withConfig additionally folds in the seed salt and row counts from forge.yaml, which change every fixture value without touching a migration, and which nothing outside Go can see.

They are returned as a pair rather than selected by a flag because the mistake this API exists to prevent is using one where the other belongs.

func ServesChoiceHint added in v0.1.45

func ServesChoiceHint(modulePath, projectName string, c config.ComponentConfig) string

ServesChoiceHint is what `forge scaffold` prints, in place of writing, when the project declares `serves` and the new component is in none.

func ServesHint added in v0.1.45

func ServesHint(componentName string) string

ServesHint is the one sentence `forge lint` and `forge scaffold` both use to say how a component served by another workload's process is marked as such.

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 ServingWorkload added in v0.1.45

func ServingWorkload(declared map[string]DeclaredWorkload, projectName string, c config.ComponentConfig) (string, bool)

ServingWorkload names the declared workload whose process ALREADY runs component c, when that workload is not c's own declaration. Two cases, and both are facts forge can read rather than guess:

  • A workload that lists c in `serves`. A subcommand that mounts several components (control-plane's `admin-api` mounts six admin services) is ONE workload; `serves` is how it says which components it runs. forge cannot see that from Go — the mount set lives in the project's own code — so the declaration states it.
  • A workload running the project binary's `server` command, which runs every service, worker and operator in one process (cmd-tree-server's MountAll + AllWorkers + AllOperators). A secondary binary is its own program and is never served by it.

Such a component needs no workload of its own: declaring one would deploy it a second time, as its own Deployment, beside the process that already serves it.

func ShouldInstrumentComponent added in v0.1.45

func ShouldInstrumentComponent(dir, ctorType, ifaceName string) bool

ShouldInstrumentComponent reports whether the component package at dir gets the generated observability decorator (and, at the composition site, the wrapped `pkg.New<Concrete>WithForgeMiddleware(pkg.New(...))` emission). It is THE single instrument decision — the compose assembler and the generate walk both route through it so they can never disagree.

ctorType is DetectConstructorType(dir); ifaceName is the component's Service-contract interface name (serviceIfaceOrDefault). A package is instrumented when ALL hold:

  1. the constructor RETURNS the Service interface (ctorType == ifaceName). Handler-shaped packages return a concrete *Service and are never wrapped — otelconnect owns the RPC edge, and wrapping *Service would change the Components field type.
  2. it has NOT opted out via a package-level `// forge:no-observe` (HasPackageNoObserveDirective) — the opt-out wins over every opt-in.
  3. it has opted IN — either the Phase-2 `// forge:constructor` marker (HasConstructorMarker) OR, for backward compatibility, the legacy owned observe_chain.go seam (DetectObserveChainSeam). Scaffolds stamp the marker AND the seam, so new packages are born instrumented; the seam keeps pre-marker projects working unchanged.

func StripKCLProse added in v0.1.45

func StripKCLProse(src string) string

StripKCLProse removes """docstrings""" and # comments from KCL source, so a scan for real declarations is not fooled by prose that illustrates them.

func SyncLegacyRootGen added in v0.1.45

func SyncLegacyRootGen(projectDir, modulePath, bin string, cs *checksums.FileChecksums) error

SyncLegacyRootGen keeps a pre-retirement command tree compiling, and reclaims root_gen.go once the tree no longer needs it.

Both directions run on every generate, so the file tracks the project's real state: a legacy project keeps a correct root_gen.go (including the migration source flipping to the embedded set when it writes its first .sql), and a project whose root.go has moved to the new shape loses the file on the next run rather than carrying a stale copy that redeclares ServiceName.

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 UnfilledDepsFor added in v0.1.45

func UnfilledDepsFor(handlerDir string) []string

UnfilledDepsFor describes each required Deps field of the handler in handlerDir that the test harness cannot construct, as "Deps.SQL (*sql.DB)". The scaffolded handler tests read it: a service with any such field gets tests that pass WithPlaceholderDeps(), so the file forge writes passes as written.

func UnmanagedMarkerNearMiss added in v0.1.45

func UnmanagedMarkerNearMiss(comment string) string

UnmanagedMarkerNearMiss returns the first `forge:*` token in a service's comment that is a misspelling of ProtoMarkerUnmanaged (`forge:unmanged`, `forge:un-managed`), or "" when there is none.

The proto-markers lint reports an unknown marker as a warning, because one is usually prose. On a service this one is not: a misspelled forge:unmanaged leaves the service managed, and the next generate scaffolds a stub for every one of its rpcs — the exact outcome the marker was written to prevent. So the descriptor path refuses a near miss outright. Known markers and tokens nowhere near this one stay prose.

func UnmappedFieldsError added in v0.1.45

func UnmappedFieldsError(fields []UnmappedField) error

UnmappedFieldsError renders a set of unmappable pairings as the error `forge generate` fails with. nil for an empty set. Every pair is listed rather than just the first, so one run tells the author about every dead field instead of one per regenerate.

func UnsatisfiableColumnsError added in v0.1.45

func UnsatisfiableColumnsError(cols []UnsatisfiableColumn) error

UnsatisfiableColumnsError renders a set of UnsatisfiableColumn findings as the error `forge generate` fails with. nil for an empty set. Every column is listed rather than just the first, so one run names every broken column instead of one per regenerate.

func UnversionableEntitiesError added in v0.1.45

func UnversionableEntitiesError(entities []UnversionableEntity) error

UnversionableEntitiesError renders findings as the error `forge generate` fails with. nil for an empty set.

This fails the build rather than warning, for the same reason UnsatisfiableColumnsError does: the generated code would compile and pass a smoke test, and only wedge on the SECOND update of a row — long after anyone would connect it to the migration that added the marker.

func UnwiredStubMarkerComment added in v0.1.45

func UnwiredStubMarkerComment(servicePkg, method string) string

UnwiredStubMarkerComment returns the whole marker doc-comment line for one handler method — `// forge:gen unwired-stub symbol=<pkg>.<Method>`. It goes LAST in the method's doc comment: revive's `exported` rule requires the FIRST line to start with the method name, so the marker cannot lead.

func ValidateBinaryConfigBindings added in v0.1.45

func ValidateBinaryConfigBindings(messages []ConfigMessage, binaries []string) error

ValidateBinaryConfigBindings refuses every (forge.v1.binary_config) binding that cannot mean what it says:

  • binary names no cmd/<name> directory. binaries is the set ProjectBinaries returned; nil skips this check (see ProjectBinaries).
  • command is set without a binary, or an element is not ONE command name (empty, contains whitespace, or is a flag).
  • two messages bind the same (binary, command). A process has ONE config at each level — one Load<Msg>, one KCL schema its env is projected from — so a second message for it has no defined meaning, and the projection silently used whichever came first.
  • a binary's root config and one of its command configs declare the same env var or flag. The process running that command loads both, so it would read one value from two declarations that can drift, and its env projection would carry the key twice.
  • a binary's root config and one of its command configs declare a top-level field of the same name. The process reads both from ONE config file, flat across the two (forge/pkg/config's file layer), so the file could not say which of them a key sets.

It reports EVERY offender rather than the first, for the reason ValidateFrontendConfigs does: a renamed binary takes several messages with it, and fixing them one generate-run at a time is miserable.

func ValidateConfigEnvAliases added in v0.1.45

func ValidateConfigEnvAliases(messages []ConfigMessage) error

ValidateConfigEnvAliases refuses every env_var_aliases entry that cannot mean what it says:

  • its name is empty or not an env var name;
  • the field has no env_var — an alias is a fallback FOR env_var;
  • it repeats the field's own env_var, or another alias of the field;
  • it names a variable another field of the same process reads, as an env_var or an alias. A process is a binary's root config, a command config together with its binary's root config (the process loads both), or the project-global config (every unbound root message);
  • the field belongs to a frontend config, whose values come from the rendered config document, never from an environment.

Two fields sharing an env_var with no alias involved is not this check's business: ValidateBinaryConfigBindings already refuses the root/command case, and the rest predates aliases.

func ValidateConfigOptionality added in v0.1.45

func ValidateConfigOptionality(messages []ConfigMessage) error

ValidateConfigOptionality refuses a field annotated `required: true` AND `optional: true`.

The two are direct contradictions — one says startup fails without a value, the other says absence is fine — and forge refuses rather than picking a winner. Either precedence would be a silent behaviour that an author has to learn from source: resolving toward `required` makes `optional` decorative on the field that most needs it to work, and resolving toward `optional` silently disables a check someone asked for. A proto author who writes both has not decided yet, and the fix is one line either way.

It reports EVERY offender rather than the first, for the reason ValidateFrontendConfigs does: these arrive in batches when a config block is split or copied, and fixing them one generate-run at a time is miserable.

func ValidateFieldOptions added in v0.1.45

func ValidateFieldOptions(optsText string) string

ValidateFieldOptions returns an inline-options block carrying ONLY the `(buf.validate.field)` options found in optsText (a field's raw `[...]` block, brackets optional), spelled as the author wrote them and collapsed to one line:

[(buf.validate.field).string.min_len = 1, (buf.validate.field).string.max_len = 120]

It returns "" when the text declares no protovalidate rule. Non-validate options in the same block (`deprecated = true`, a custom extension) are dropped — the caller is copying VALIDATION onto another message, not the field's whole option set.

Verbatim, rather than re-rendering FieldConstraints: FieldConstraints is deliberately the lossy SQL/zod-projectable SUBSET, but the wire's target IS proto, so nothing needs to be dropped. Copying the text carries the rules forge does not project either — CEL, uuid/hostname/ip and the other string formats, in/not_in, const, repeated/map/duration rules — so a rule declared once is enforced on the wire wherever the field travels.

Only FIELD options are carried. Message-level rules (`option (buf.validate.message).cel`) are the one shape that can name sibling fields, and are never lifted onto a request that carries a different field set.

func ValidateFrontendConfigs added in v0.1.45

func ValidateFrontendConfigs(messages []ConfigMessage) error

ValidateFrontendConfigs refuses a `sensitive` field that reaches a frontend config.

This is the highest-value rule in the frontend config system, and it is an ERROR rather than a warning on purpose. Everything a browser reads is public: a frontend's config is delivered to the client, so a secret in it is a published secret the moment the artifact ships. A warning would be read after the fact — and a leaked credential cannot be un-leaked, so the only useful moment to refuse is before the value is ever written into a bundle, a config.js, or a rendered KCL plan.

It reports EVERY offender rather than the first, because a config split gone wrong tends to produce several at once and fixing them one generate-run at a time is miserable.

The message is a runbook: it names the field, where it was declared (following composition into shared blocks, which is where this mistake actually happens — a BaseConfig holding both an issuer and a client secret, composed onto a frontend), and what to do about it.

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 scaffold webhook` scaffolds webhook_<name>.go (+ _test.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. Best-effort: a missing directory yields nil.

Three exclusions, and two are load-bearing. forge's OWN output for this discovery is webhook_routes_gen.go, which sits in the same directory and matches the same webhook_*.go prefix. Without the *_gen.go exclusion the second `forge generate` discovers a phantom webhook named "routes_gen" and emits a route for s.handleWebhookRoutesGen, which nothing declares:

internal/handlers/widget/webhook_routes_gen.go:17:67:
    s.handleWebhookRoutesGen undefined

webhook_store.go is excluded for the same reason on a longer timescale: the dedupe store now lives in forge/pkg/middleware and is no longer scaffolded, but projects generated before that still carry the file on disk, and it would otherwise be read as a webhook named "store".

func WorkloadDeclared added in v0.1.45

func WorkloadDeclared(content, name string) bool

WorkloadDeclared reports whether workloads.k content declares a workload by that name, ignoring prose that merely mentions it.

Exported for callers that need to ask what a project DECLARES rather than what it has on disk in some other format.

func WorkloadKindFor added in v0.1.45

func WorkloadKindFor(componentKind string) string

WorkloadKindFor maps a scaffolded component kind onto the KCL workload kind that deploys it.

The two axes are NOT the same, which is the whole reason this function exists rather than a cast:

  • config.ComponentKindServer names a Connect-RPC surface — a code fact. It deploys as a `service` (Deployment + Service).

  • config.ComponentKindBinary names a second cmd/<name>/main.go — a build fact. It deploys as a `tool`: forge builds it into the image, but nothing schedules it. It is run on demand (`kubectl exec`, a CI step), which is exactly what the old `binary` deploy kind meant in practice — its manifests were an unscheduled Deployment nobody addressed.

  • config.ComponentKindCron is a worker whose process runs an IN-PROCESS scheduler (worker-cron: robfig/cron, Start blocks until shutdown). It deploys as a long-running `worker`. A Kubernetes CronJob would start that process on the schedule and wait for an exit that never comes.

An unrecognized kind falls back to `service`: a plain Deployment+Service is the least surprising thing to render for something forge has no name for.

func WorkloadName added in v0.1.46

func WorkloadName(component string) string

WorkloadName is the `name` a component's workload declares in deploy/kcl/workloads.k: the component name as a DNS-1123 label, which fw.Workload requires because it becomes every object's name. A component is named like a Go package (`engine_shadow`, `admin_server`); its workload is `engine-shadow`, declared under the KCL identifier `engine_shadow`.

KCLIdentifier(WorkloadName(c)) == KCLIdentifier(c) for every component name, so code that matches a declared workload to a component compares the two through KCLIdentifier (SameWorkload) and never depends on the spelling.

func WorkloadStanza added in v0.1.45

func WorkloadStanza(modulePath, projectName string, c config.ComponentConfig) string

WorkloadStanza renders ONE workload as the typed KCL literal that belongs in deploy/kcl/workloads.k. It is the single formatter behind both the initial scaffold and `forge scaffold <kind>`'s append, so a workload added later is indistinguishable from one the project was born with.

The emitted declaration is COMPLETE and RUNTIME-INDEPENDENT (ADR 0002): the build, the subcommand in `args`, the ports, the credentials it reads. It says nothing about WHERE it runs — each env binds it to a runtime (`forge.OnHost`, `OnCluster`, `OnHosted`, ...) and the same `args` select the subcommand on every one: `go run ./cmd/<p> <args>` on the host, the image's ENTRYPOINT `/app/<p>` plus `<args>` in a pod. Per-env facts (replicas, resources, env values) are refined in deploy/kcl/<env>/main.k.

No probes: the lowering writes `/readyz` + `/healthz` on the `http` port into the spec of every service forge builds, on every runtime, which is exactly what serverkit serves.

func WorkloadStanzaHint added in v0.1.45

func WorkloadStanzaHint(modulePath, projectName string, c config.ComponentConfig) string

WorkloadStanzaHint is the message shown when the stanza could not be appended automatically: the exact text to paste, and where.

func WorkloadsKCLExists added in v0.1.45

func WorkloadsKCLExists(projectDir string) bool

WorkloadsKCLExists reports whether the project already declares its workloads. Callers use it to decide between scaffolding the file and appending to it; neither path ever overwrites existing content.

func WriteTierKCL added in v0.1.45

func WriteTierKCL(forgeRoot, pkgPath string) (string, error)

WriteTierKCL regenerates kcl/tiers/tiers_gen.k in forgeRoot.

Types

type AmbiguousProvider added in v0.1.45

type AmbiguousProvider struct {
	// Component is the consuming component's FieldName.
	Component string
	// Field is the Deps field name.
	Field string
	// Type is the declared field type.
	Type string
	// Candidates are the FieldNames of every component whose constructed
	// value is assignable to Type, sorted.
	Candidates []string
}

AmbiguousProvider records a Deps field whose interface type is satisfied by more than one registered component, so forge cannot choose a producer.

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 AuthSetupTemplateData added in v0.1.45

type AuthSetupTemplateData struct {
	Module string
	// Graph renders the composition-graph form: APIKeyValidator takes the
	// serving pool (*DB), not the legacy *Infra.
	Graph       bool
	HasDatabase bool
}

AuthSetupTemplateData is the data shape for the owned internal/app/auth.go scaffold (app-auth.go.tmpl).

type BinaryConfig added in v0.1.45

type BinaryConfig struct {
	// Binary is the cmd/<name> leaf this config belongs to.
	Binary string
	// Command is the cobra path below Binary whose process loads this
	// config (["server", "api"]); empty for the binary's ROOT config. See
	// ConfigMessage.Command.
	Command []string
	// MessageName is the config message's proto name (e.g. "AdminConfig") —
	// the generated Go type this binary's Config aliases.
	MessageName string
	// Fields is every leaf this config resolves, composed blocks included,
	// each with its GoPath set. This is what the process's env projection and
	// its Deployment's env vars are built from: the union of what the
	// process actually reads, and nothing else. Every one of them registers
	// in ONE flag set — persistent on the root for a root config, local on
	// the subcommand for a command config — so composition shares a
	// DEFINITION, never a flag home.
	Fields []ConfigTemplateField
	// Blocks are the config blocks this message composes, in declaration
	// order — the shared definitions, named so the template can reach them.
	Blocks []ConfigBlockRef
	// BinaryHasRootConfig reports, on a command config, whether its binary
	// also binds a root config. That root config owns the --config file flag
	// (persistent on the binary's root), so the command config registers its
	// leaves alone rather than a second --config shadowing the inherited one.
	BinaryHasRootConfig bool
}

BinaryConfig is one binary's complete config surface: the message that carries its (forge.v1.binary_config).binary annotation, resolved into the shape the templates and the KCL projection consume.

One of these per binary is what makes ownership DISJOINT. Every consumer — the Go loader shim, the KCL schema, the env projection — is built from exactly one BinaryConfig, so a field deleted from one binary's message is absent from that binary's flags, schema and Deployment env and reaches no other binary.

func BinaryConfigsFromMessages added in v0.1.45

func BinaryConfigsFromMessages(messages []ConfigMessage) []BinaryConfig

BinaryConfigsFromMessages resolves the per-binary config surfaces from a project's parsed config messages: one BinaryConfig per message carrying a (forge.v1.binary_config) annotation — a binary's root config or one of its command configs — in declaration order.

A project that annotates nothing gets NO BinaryConfigs, and every caller falls back to the single project-global AppConfig path — which is why per-binary config is additive and the common single-binary project is unchanged. Selection is by ANNOTATION, never by message name.

func (BinaryConfig) CommandGoArgs added in v0.1.45

func (bc BinaryConfig) CommandGoArgs() string

CommandGoArgs renders Command as Go string-literal arguments (`"server", "api"`), for the generated loader's path check.

func (BinaryConfig) CommandLine added in v0.1.45

func (bc BinaryConfig) CommandLine() string

CommandLine is the subcommand path alone ("server api"); "" for a root config.

func (BinaryConfig) PathEnv added in v0.1.45

func (bc BinaryConfig) PathEnv() string

PathEnv is the environment variable that names this config's file when --config is not given: its binary's <BINARY>_CONFIG, shared by the binary's root config and every one of its command configs, and derived by the runtime from the same (forge.v1.binary_config).binary.

func (BinaryConfig) Process added in v0.1.45

func (bc BinaryConfig) Process() string

Process is the process this config is loaded by, as typed at a shell: "reliant" for a root config, "reliant server api" for a command config.

type BinaryConfigBindingProblem added in v0.1.45

type BinaryConfigBindingProblem struct {
	// Message is the config message carrying the annotation.
	Message string
	// Where is "proto/<file>:<line>", or "" when the descriptor carries no
	// source position.
	Where string
	// Binding renders the annotation as written, e.g.
	// `{binary: "reliant", command: ["server", "api"]}`.
	Binding string
	// Detail explains this one offender: what is wrong and, where forge can
	// tell, what was probably meant.
	Detail string
	// contains filtered or unexported fields
}

BinaryConfigBindingProblem is one refused binding.

type BinaryConfigFields added in v0.1.45

type BinaryConfigFields struct {
	Binary string
	// Command is the subcommand path whose process loads this config; empty
	// for the binary's root config. See ConfigMessage.Command.
	Command     []string
	MessageName string
	Fields      []ConfigField
}

BinaryConfigFields is one binary-bound config's KCL projection input: the config message name (which names the schema and lambda), the process that loads it, and the ConfigFields it resolves — its own leaves plus the leaves of every block it composes.

func BinaryConfigFieldsFrom added in v0.1.45

func BinaryConfigFieldsFrom(messages []ConfigMessage) []BinaryConfigFields

BinaryConfigFieldsFrom resolves the per-binary KCL projection inputs from parsed config messages, in declaration order. Empty for a project that declares no per-binary configs.

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 the constructor returns (T, error)
	// Constructor is the component's entry-point func name — `New` unless the
	// package moved the `// forge:constructor` marker onto a name of its own
	// (Open / Connect / NewReadOnly). Read through [BootstrapComponentData.ConstructorName]
	// so a literal built without it still emits the canonical shape.
	Constructor string
	// 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
	// HasDB records whether this component's Deps struct declares a
	// `DB orm.Context` field. The test-harness template gates `DB: cfg.db`
	// on it, exactly as HasLogger/HasConfig gate their fields. Without it
	// NewTest<Package> built the interactor with a nil DB while the
	// sibling NewTest<Service> in the same generated file merged one in —
	// so an interactor test that touched the database nil-panicked inside
	// a DO NOT EDIT file the author cannot fix.
	HasDB 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.

func (BootstrapComponentData) ConstructorName added in v0.1.45

func (d BootstrapComponentData) ConstructorName() string

ConstructorName is the constructor selector templates must emit for this component: the detected name, or the canonical `New` when the field was never populated (hand-built literals in tests, pre-disk-resolution paths). Templates call `{{.ConstructorName}}` so an unset field can never render an empty selector.

type BootstrapOperatorData

type BootstrapOperatorData = BootstrapComponentData

BootstrapOperatorData is the operator spelling of BootstrapComponentData. Operators and workers render from the same shape; the alias keeps the operator-side call sites reading as operator code.

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)
	// ConnectServiceGoName is protoc-gen-connect-go's base for the service
	// (naming.ConnectServiceGoName): the test client is <base>Client.
	// "Alphav1connectService" → Alphav1ConnectService.
	ConnectServiceGoName   string
	ProtoConnectImportPath string // e.g. "github.com/foo/bar/gen/services/api/v1/apiv1connect"
	ProtoConnectPkg        string // e.g. "apiv1connect" (Go identifier used at call sites)
	// MountMethod is the handler method that mounts the service — "Register"
	// normally, "Mount" when the service declares an RPC of its own called
	// Register (one type cannot carry both, and the RPC wins).
	MountMethod string
	Fallible    bool // true if the constructor returns (T, error)
	HasDB       bool // true if Deps struct has a DB orm.Context field
	// Constructor is the handler's entry-point func name — `New` unless the
	// package moved the `// forge:constructor` marker onto a name of its own.
	// Read through [BootstrapTestServiceData.ConstructorName].
	Constructor string
	// 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
	// FuncDefaults lists required func-typed Deps fields the harness fills
	// with a real framework default (Clock -> time.Now, IDGen -> ulid). A
	// func value can't be synthesized by the interface auto-stubber, so
	// without this a required `Now func() time.Time` / `NewID func() string`
	// would leave NewTest<Svc> constructing with a nil func — validateDeps
	// (or the first call) then fails. Optional func fields are excluded
	// (they stay nil by design — the `//forge:optional-dep` contract).
	FuncDefaults []DepsFuncDefault
	// FuncTodos lists required func-typed Deps fields the harness has NO
	// sensible default for (e.g. `Send func(string) error`). Emitted as a
	// TODO next to NewTest<Svc> so the author overrides via With<Svc>Deps
	// instead of hitting a silent nil-func validateDeps failure — the
	// func-typed analog of UnresolvedStubs.
	FuncTodos []UnresolvedAutoStub
	// UnfilledDeps lists every required Deps field the harness leaves nil
	// because it cannot construct one (see computeUnfilledDeps). Each gets
	// a WithPlaceholderDeps fill and a line in the construction failure.
	UnfilledDeps []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).

func (BootstrapTestServiceData) ConstructorName added in v0.1.45

func (d BootstrapTestServiceData) ConstructorName() string

ConstructorName is the constructor selector the testing.go template must emit for this service — the detected name, or the canonical `New` when the field was never populated.

type BootstrapTestingGenInput

type BootstrapTestingGenInput struct {
	GenContext

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

GenerateBootstrapTesting generates the per-component test harness: one helpers_gen_test.go per service / internal package, rendered from component_test_helpers.go.tmpl into that component's own directory.

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

BootstrapTestingGenInput embeds GenContext (ProjectDir / ModulePath / Checksums) and adds the component inventory. 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
	// DepsKeys is every field name the component's `Deps{…}` literal may
	// legally key — the full set including the embedded and unexported fields
	// Deps deliberately drops (see DepsLiteralKeys). DepsFound reports whether
	// a Deps struct was positively READ, so an empty DepsKeys can be told apart
	// from a package that failed to parse. Only the compose reconciler's
	// stale-key drop reads these, and only when DepsFound is true.
	DepsKeys  map[string]bool
	DepsFound bool
	// 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 project map / audit use this as a guardrail signal.

type CRDDoc added in v0.1.45

type CRDDoc struct {
	// Kind is the CR kind ("Workload"), used for the lambda name and
	// for deterministic ordering.
	Kind string
	// Lambda is the generated KCL lambda name ("simplebackend_crd"). It
	// matches the historical hand-authored spelling so a project migrating
	// off the hand-written module only changes its import line.
	Lambda string
	// CRD is controller-tools' projection of the Go types.
	CRD apiext.CustomResourceDefinition
}

CRDDoc is one CRD's projected form: the kind it declares and the fully rendered apiextensions.k8s.io/v1 object, ready to lower into KCL.

func LoadCRDsFromGoTypes added in v0.1.45

func LoadCRDsFromGoTypes(projectDir string) ([]CRDDoc, error)

LoadCRDsFromGoTypes projects every CRD the project installs (see CRDSourceRoots) into its apiextensions form, using controller-tools' own marker collection and schema derivation.

It returns nil (no error) when the project declares no CRD types at all: most forge projects have no operator, and their `forge generate` must not fail or emit an empty module.

type CRDListEdit added in v0.1.45

type CRDListEdit int

CRDListEdit is what AppendOperatorCRD did to an operator's `crds` list.

const (
	// CRDListAdded means the kind was appended.
	CRDListAdded CRDListEdit = iota
	// CRDListAlreadyListed means the list already names the kind; nothing
	// was written.
	CRDListAlreadyListed
	// CRDListNotEditable means there is no workloads.k, no workload of that
	// name, or no `crds = [...]` in it forge can edit unambiguously. Nothing
	// was written; the caller prints the edit.
	CRDListNotEditable
)

func AppendOperatorCRD added in v0.1.45

func AppendOperatorCRD(projectDir, workloadName, kind string) (CRDListEdit, error)

AppendOperatorCRD adds kind to the `crds` list of the operator workload named workloadName in deploy/kcl/workloads.k — what `forge scaffold crd` calls, so the CRD it writes is one the operator's derived ClusterRole covers. `crds` is the only list that derivation reads the kinds from, and an operator scaffolded before its first CRD declares `crds = []`.

Additive like AppendWorkloadStanza: the one new entry, written in the style the list already uses (appendKCLListElement); nothing else moves.

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" — the Go method the connect handler interface declares
	InputType  string // "CreatePatientRequest" — pb Go type
	OutputType string // "CreatePatientResponse" — pb Go type
	EntityName string // "Patient" — the db.<Entity> struct and db.Create<Entity> funcs
	// EntityPbType is the entity's generated pb message type. It equals
	// EntityName unless the proto name is not already in protoc-gen-go's
	// casing ("Oauth2token" → pb.Oauth2Token, db.Oauth2token).
	EntityPbType string
	EntityLower  string // "patient"
	Operation    string // "create", "get", "list", "update", "delete"
	// AuthRequired is the RPC's (forge.v1.method).auth_required, true
	// unless the proto explicitly opts out. It gates nothing — it selects
	// which sentence the method's comment shows the handler author, so
	// the scaffold states the RPC's declared intent next to the fact that
	// the delegation does not honour it.
	AuthRequired bool
	// AuthSeam is the app-owned function that resolves the caller, e.g.
	// "middleware.GetUser". Rendered rather than spelled in the template
	// so renaming the seam moves every scaffolded mention with it.
	AuthSeam     string
	PkField      string // "Id" — the PK's field on the pb request/entity message
	PkDBField    string // "Id" — the PK's field on the db.<Entity> struct (naming.ColumnGoName)
	PkColumnName string // "id" (raw DB column name)
	PkGoType     string // "int64"
	HasPkInInput bool   // true if the request message likely has an ID field
	// ResponseField is the pb Go field of the response that carries the
	// entity — the single entity for create/get/update ("Patient"), the
	// repeated one for list ("Patients"). Read off the response message's
	// own fields, so an entity whose name protoc-gen-go re-cases
	// ("LLMKey" → field llm_key → LlmKey) packs the field that exists.
	ResponseField   string
	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
	// HasDescending is true when the list request declares a companion
	// `bool descending` field alongside order_by. The OrderBy closure only
	// dereferences req.Descending when this is set; an order_by-only request
	// (no descending field) generates a closure that returns a constant
	// false (ascending) so the generated app compiles instead of referencing
	// a nonexistent req.Descending.
	HasDescending bool
	// HasTotalCount is true when the list RESPONSE message declares a
	// total_count field (the entity scaffolder emits one). It wires a COUNT
	// over the same filters so the response reports the real total instead of
	// a constant 0.
	HasTotalCount     bool
	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
	// ReadOnlyColumns names the entity's columns whose WIRE field carries
	// `forge:read-only` or `forge:computed` — present only on an "update"
	// method. The generated UpdateOp carries them as op.ReadOnly (an
	// update_mask naming one is refused) and its Persist passes them to
	// crud.Preserve (a full replace leaves them as stored), so a client can
	// neither set a server-owned column nor reset it by omitting it.
	//
	// Read from the proto on every generate, not declared in the
	// migration: who may write a column THROUGH THE API is a fact about the
	// API, and the storage layer's own write policy (forge:immutable,
	// ,skipupdate) is left alone because the custom RPCs that own these
	// columns write them through the same repository. See ReadOnlyColumns.
	ReadOnlyColumns []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 WIRED but naive entity query (best-effort filters
	// onto declared columns, response left TODO) rather than CRUD-body
	// code that wouldn't compile against the real proto, and the user
	// refines the custom shape in the owned shim. See validateCRUDShape
	// for the rules. It carries a `forge:custom-read-shape` marker plus
	// MismatchReason so the user (and `forge project audit`, under
	// crud_stubs) can spot it. That is a DIFFERENT marker from
	// `forge:gen unwired-stub` on purpose: this method is wired and
	// returns rows, so the readers of the unwired-stub marker — excision,
	// orphan_stubs, out-of-tree orchestrators — must not claim it as
	// unimplemented. (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
	// FillHandlerColumns names the entity's `forge:fill=handler` columns —
	// present only on a "create" method, and only when at least one such
	// column exists. When non-empty, ensureCRUDShimFile scaffolds an
	// op.Entity wrapper around the create delegation carrying a
	// FORGE_SCAFFOLD reminder to stamp each one, instead of the bare
	// crud.HandleCreate(...) delegation.
	FillHandlerColumns []string
	// OwnerColumn names the entity's `forge:owner` column — the column
	// that says which principal a row belongs to — or "" when the table
	// declares none.
	//
	// It changes no generated behavior: forge injects no WHERE clause,
	// because only the application knows how a caller's claims map onto
	// this column's values. What it does is aim the shim's scaffolded
	// reminder at a NAMED column, with middleware.GetUser already called
	// and the op seam already wrapped, so satisfying the `unscoped_auth`
	// gate is filling in one expression rather than writing ten lines.
	//
	// Only the first such column is carried. A table with two owner
	// columns is expressing a composite scope this scaffold cannot guess
	// at, and the FORGE_SCAFFOLD marker still fails lint until a person
	// writes the real predicate.
	OwnerColumn string
	// OwnerField is OwnerColumn as the Go field name on the generated
	// entity struct (e.g. "company_id" -> "CompanyId"), for the Create
	// scaffold, which stamps the column rather than filtering on it.
	OwnerField string
	// Scoped is true when the shim should scaffold the scoping wrapper:
	// an OwnerColumn exists AND the RPC is authenticated.
	//
	// Both halves, because they are the same two halves the
	// `unscoped_auth` gate compares. An owned table reached by a
	// deliberately public RPC is not a finding there and must not get a
	// GetUser call here — that call would reject the credential-less
	// callers the proto advertises as welcome, which is forge quietly
	// overruling a declaration the author made.
	Scoped bool
}

CRUDMethodTemplateData holds per-method template data.

Every name here is a Go identifier the rendered code spells, so each is derived with the rule of whoever DECLARES that identifier: MethodName, InputType, OutputType, EntityPbType and the pb field names (PkField, ResponseField, UpdateEntityField, UpdateMaskField, filter GoNames) are protoc-gen-go/connect-go's (naming.GoCamelCase, naming.GoFieldNames); EntityName and PkDBField/OwnerField are forge's db.<Entity> struct's. The proto spellings stay on CRUDMethod.Method.

type CRUDProjectionReference added in v0.1.45

type CRUDProjectionReference struct {
	// At is `file:line` within the handler package.
	At string
	// Symbol is what it uses: the op (`crudCreateTaskOp`) or, for a Create
	// rpc, its create-request test factory (`NewCreateTaskRequest`).
	Symbol string
}

CRUDProjectionReference is one place a handler package still uses something forge generates for an rpc only while that rpc is CRUD.

type CRUDResult added in v0.1.46

type CRUDResult struct {
	// Wrote is true when the call emitted (or refreshed) its file.
	Wrote bool
	// Skipped, when Wrote is false, says why nothing was written, phrased to
	// follow "nothing written: ".
	Skipped string
	// Routine marks a skip that is the steady state on every re-run (the
	// scaffold-once test already exists), reported only under --verbose.
	Routine bool
}

CRUDResult is what one CRUD generation call did, so its caller reports that rather than what it asked for. A run used to print "✅ Generated handlers/<svc> CRUD wiring" and "✅ Generated …/handlers_crud_test.go" after calls that returned early having written nothing — every rpc was hand-written, or the scaffold-once test was already born — and the files it named did not exist.

func GenerateCRUDHandlers

func GenerateCRUDHandlers(svc ServiceDef, crudMethods []CRUDMethod, modulePath string, projectDir string, cs *checksums.FileChecksums) (CRUDResult, 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 project audit`. A nil cs is tolerated.

func GenerateCRUDTests

func GenerateCRUDTests(svc ServiceDef, crudMethods []CRUDMethod, modulePath string, projectDir string, cs *checksums.FileChecksums) (CRUDResult, 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 / 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.

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 is true when the ops file names pkg/orm — which every real
	// op now does, because the get/update/delete scoping seams are typed
	// in orm.QueryOption. Also set by a list feature or a jsonb create
	// conversion on a service whose methods are all custom-shaped.
	NeedsORM bool
	// NeedsCRUDLib is true when at least one method emits a real CRUD
	// body (i.e. uses pkg/crud and internal/db). 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
	// NeedsFmt gates the fmt import (set when any conversion's read path
	// emits the corrupt-enum guard, which calls fmt.Errorf).
	NeedsFmt bool
	// NeedsTime gates the stdlib time import (set when any conversion
	// formats or parses a legacy-TEXT timestamp column's RFC3339Nano text).
	NeedsTime 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
	HasTimestamps bool // table has created_at AND the proto message exposes it — created_at is asserted set on the wire
	// 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. 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)
	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    // variant 0's literal: `"test-value"`, a seeded FK id, a CHECK-pool value, ...
	// TestValue2 is variant 1's literal — distinct from TestValue wherever
	// the column admits a second value, so a UNIQUE index added later cannot
	// collide the two rows a test creates.
	TestValue2 string
}

CRUDTestFieldData holds per-field data for generating test values.

type CRUDTestTemplateData

type CRUDTestTemplateData struct {
	// UnfilledDeps mirrors ServiceTemplateData.UnfilledDeps: when the
	// harness cannot construct a required Deps field, the lifecycle test
	// passes WithPlaceholderDeps() so it constructs as written.
	UnfilledDeps []string
	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"
	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 `NewTest<X>` /
	// `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
	// ServiceImportPath is the internal/handlers/ directory leaf, used to
	// import the handler package whose in-package helpers_gen_test.go
	// declares NewTest<X>. The scaffolded test stays `package <pkg>_test`
	// (external), and Go compiles a package's in-package _test.go files
	// INTO the package under test — so the external test package sees
	// these helpers through this ordinary import.
	ServiceImportPath string
}

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

type CallerResolver added in v0.1.45

type CallerResolver struct {
	// ImportPath is the declaring package's import path.
	ImportPath string `json:"import_path"`
	// Package is the declaring package's name, which is the identifier an
	// unaliased import binds.
	Package string `json:"package"`
	// Func is the function's name.
	Func string `json:"func"`
	// File and Line locate the declaration (File project-relative).
	File string `json:"file"`
	Line int    `json:"line"`
}

CallerResolver is one function a project declared with ResolvesCallerDirective.

func (CallerResolver) String added in v0.1.45

func (r CallerResolver) String() string

String renders the resolver as code calls it: `auth.GetUserIDFromContext`.

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

	// MountMissing is true when internal/app declares a mount surface but
	// NOT (*Components).Mount<MountFieldName> — a project that disowned
	// mounts_services_gen.go and owns the surface by hand. The mount file then
	// emits a placeholder that compiles and refuses at run time with the exact
	// method to add, instead of a method expression that breaks the build (and
	// with it every `forge generate` and `forge scaffold service`).
	MountMissing bool
}

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
	// Graph renders the composition-graph composition root: no command
	// groups, `server` runs the server root.
	Graph     bool
	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

	// Name is the primary binary's name — the cmd/<Name>/ leaf. The
	// scaffold-once command-tree templates reference it when they explain
	// where a file sits or what a command is invoked as (`<Name> server`,
	// the `-X <module>/cmd/<Name>/cmd.version=` ldflags path). Derived from
	// the cmd tree on disk by primaryCmdTreeDir, not re-read from
	// forge.yaml, so it agrees with the directory the file is written into
	// even for a project whose binary was renamed.
	Name string

	// 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. The generated runServer calls the OWNED app.SetupAuth() unconditionally (auth is code now, not config), so no auth-provider field is threaded here.

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 scaffold 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

	// Optional is (forge.v1.config).optional: the application tolerates this
	// field being unset. For a SENSITIVE field it is the only way to say so —
	// a declared secret_ref with no value in the store is an error by
	// default, because discovering a missing credential at runtime is the
	// failure that check exists to prevent. `json:",omitempty"` keeps old
	// descriptors readable (additive contract).
	Optional bool `json:",omitempty"`

	// 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()) 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"`

	// ProtoFile is the path of the proto file declaring this field, e.g.
	// "proto/config/v1/config.proto". It carries no codegen behavior: it
	// exists so a generate-time refusal can NAME the sources it is talking
	// about. Fields from several protos are flattened into one KCL schema,
	// so when two of them collide the field name alone does not tell an
	// author which protos to go edit (see CheckDuplicateConfigFields).
	// `json:",omitempty"` keeps descriptors written by older forge binaries
	// readable (additive contract, see the audit-json skill).
	ProtoFile string `json:",omitempty"`

	// EnvVarAliases is (forge.v1.config).env_var_aliases: the further env
	// vars the runtime loader reads, in order, when EnvVar is unset. No
	// generator projects them — the KCL env map, the secret store key and the
	// flag's help name EnvVar alone, so forge's own setters converge on one
	// name — but generate refuses an alias that collides with another name
	// the same process reads (ValidateConfigEnvAliases). `json:",omitempty"`
	// keeps old descriptors readable (additive contract).
	EnvVarAliases []EnvVarAlias `json:",omitempty"`

	// KCLBlock / KCLBlockType place a composed config block's LEAF in the KCL
	// projection: KCLBlock is the composing field's name on the root message
	// (`stripe` for `StripeConfig stripe = 40;`) and KCLBlockType the block
	// message (`StripeConfig`). Set only by the block flattener
	// (config_block_flatten.go); empty for a root-level field. The KCL
	// emitters project such a leaf into a NESTED schema — authored as
	// `stripe.secret_key` — so two blocks may each declare a leaf of the same
	// name. Never serialized: it is derived from MessageType on every run.
	KCLBlock     string `json:"-"`
	KCLBlockType string `json:"-"`
}

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

func ConfigFieldsForMessage added in v0.1.45

func ConfigFieldsForMessage(messages []ConfigMessage, name string) []ConfigField

ConfigFieldsForMessage returns the raw ConfigFields one binary-bound config message resolves — its own leaves plus the leaves of every block it composes — in the order the KCL emitters expect. It is the ConfigField- level twin of BinaryConfig.Fields, kept separate because the KCL projection works from the parsed proto fields (env_var/sensitive/default), not the Go template shape.

This is what makes a workload's Deployment carry only ITS process's env vars: the projection for a config is emitted from exactly this set.

Keyed on the MESSAGE, not the binary: one binary binds several messages (its root config and one per command), and a lookup by binary returned the first message's fields for all of them.

func FlattenBlockLeaves added in v0.1.45

func FlattenBlockLeaves(messages []ConfigMessage, rootMessage string) []ConfigField

FlattenBlockLeaves projects rootMessage's fields into the field list the KCL emitters consume: its own scalar leaves, plus one level of the leaves of every config block it composes, each tagged with the block it belongs to (ConfigField.KCLBlock). Names are never rewritten.

func FlattenFieldsWithBlocks added in v0.1.45

func FlattenFieldsWithBlocks(fields []ConfigField, messages []ConfigMessage) []ConfigField

FlattenFieldsWithBlocks is FlattenBlockLeaves for a caller that already holds the field list — the per-binary and multi-root-message emitters, which select their fields by a rule of their own before flattening.

func (ConfigField) KCLPath added in v0.1.45

func (f ConfigField) KCLPath() string

KCLPath is the dotted path a config.k instance authors this field by, and the lambda reads it by: `secret_key` for a root field, `stripe.secret_key` for a leaf of the `stripe` block.

type ConfigInstanceBlocks added in v0.1.45

type ConfigInstanceBlocks struct {
	Var    string
	Fields []ConfigField
}

ConfigInstanceBlocks describes one config instance a config.k declares: its variable name (`app_config`), and the root/block fields of its schema.

func ConfigInstancesForMigration added in v0.1.45

func ConfigInstancesForMigration(rootFields []ConfigField, perBinary []BinaryConfigFields) []ConfigInstanceBlocks

ConfigInstancesForMigration names the config.k instances forge scaffolds: `app_config` for the root AppConfig and `<snake(message)>` per binary.

type ConfigMessage

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

	// Binary is the cmd/<name> leaf this config belongs to, from the
	// message-level (forge.v1.binary_config).binary annotation. Empty means
	// the message is not bound to a binary: either the project's single
	// project-global AppConfig (the common case — nothing to annotate) or a
	// shared block composed onto other configs (BaseConfig).
	//
	// It is what makes per-binary ownership DISJOINT: a binary's generated
	// loader, KCL schema and env projection are built from the message
	// carrying its name, so deleting a field from one binary's config
	// cannot reach another's. Keyed on the ANNOTATION, never on the message
	// NAME — renaming AdminConfig does not re-point it at another binary.
	// `json:",omitempty"` keeps descriptors written by older forge binaries
	// readable (additive contract, see the audit-json skill).
	Binary string `json:",omitempty"`

	// Command is the cobra command path below Binary whose process loads
	// this config, from (forge.v1.binary_config).command — ["server", "api"]
	// for `reliant server api`. Empty means the binary's ROOT config: the
	// binary-wide surface every one of its processes loads. Non-empty means
	// exactly the process running that subcommand loads it, alongside the
	// root config. A binding is keyed on (Binary, Command).
	Command []string `json:",omitempty"`

	// Frontend is the forge.yaml frontends[].name whose BUNDLE loads this
	// config, from the message-level (forge.v1.frontend_config).frontend
	// annotation. Empty means the message is not bound to a frontend.
	//
	// It plays the same disjoint-ownership role Binary does, one layer out:
	// a frontend's generated TypeScript module, KCL schema and per-env
	// projection are built from the message carrying its name. The
	// difference is what the binding IMPLIES about secrecy — a
	// frontend-bound message is delivered to a browser, so every field it
	// carries is public and a `sensitive` field reaching one is a
	// generate-time error (see ValidateFrontendConfigs).
	//
	// A message may carry BOTH annotations only if that is genuinely
	// intended; the shared-definition pattern is to compose one unannotated
	// block (BaseConfig) onto an annotated binary config AND an annotated
	// frontend config, so the shared fact is declared once.
	Frontend string `json:",omitempty"`

	// ProtoFile and Line locate the message's (forge.v1.binary_config)
	// option — or the message itself when the option has no recorded
	// position — so a generate-time refusal of the binding names the line
	// an author edits (see ValidateBinaryConfigBindings). ProtoFile is
	// relative to the buf module root ("config/v1/config.proto"), as on
	// ConfigField; Line is 1-based and 0 when unknown (a request without
	// source info). Neither carries codegen behavior.
	ProtoFile string `json:",omitempty"`
	Line      int    `json:",omitempty"`
}

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.

func (ConfigMessage) ProcessName added in v0.1.45

func (m ConfigMessage) ProcessName() string

ProcessName renders the process a binding names, as it is typed at a shell: "reliant" for a root config, "reliant server api" for a command config. Empty for a message bound to no binary.

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
	// BlockAliases are the block types pkg/config re-exports under their own
	// names (`type TraderConfig = configv1.TraderConfig`), so a component
	// takes its block as `Cfg *config.TraderConfig` and compose.go fills it
	// from the loaded Config by type. A block whose name pkg/config already
	// declares for something else (Mode, a per-binary config) is left out
	// rather than redeclared.
	BlockAliases []string
	// RoleModeField is the Go field name of the field tagged
	// role=CONFIG_FIELD_ROLE_MODE, or "" when no field carries it. The
	// generated Mode() reads 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

	// BinaryConfigs are the per-binary config surfaces, one per config
	// message carrying a (forge.v1.binary_config).binary annotation. EMPTY
	// for the common project that annotates nothing — config.go.tmpl then
	// emits exactly what it always did, so per-binary config costs the
	// single-binary case nothing.
	BinaryConfigs []BinaryConfig

	// RootConfigMessage names the project-global config message that
	// `Config`, `RegisterFlags` and `Load` are emitted for — "AppConfig" in
	// every scaffolded project. It is empty only when a project has moved
	// ENTIRELY to per-binary configs and deleted its AppConfig, in which
	// case the template emits the per-binary surfaces alone rather than
	// aliasing a type that no longer exists.
	RootConfigMessage string

	// PrimaryConfigMessage names the BinaryConfig bound to the project's
	// PRIMARY binary (the one with cmd/<bin>/cmd/root.go — the server
	// binary every scaffolded project has), used ONLY when
	// RootConfigMessage is empty.
	//
	// The primary binary's own compose graph — providers.go, compose.go,
	// mounts_services_gen.go, the cmd-tree-*.go.tmpl scaffold, every
	// service/worker/internal-package Deps — references `config.Config` /
	// `config.RegisterFlags` / `config.Load` / `config.Validate` /
	// `config.ModeOf` unconditionally; those templates were never written
	// to be all-binary-aware because in the common (single root AppConfig)
	// project there is nothing else for them to say. Rather than teach 20+
	// templates a second vocabulary, config.go.tmpl aliases that SAME
	// symbol set onto whichever message the primary binary itself loads —
	// which is also the semantically right binding: the server's
	// component graph should read the server's own config, not a
	// different binary's.
	//
	// Set by GenerateConfigLoader (never by BuildConfigTemplateData, which
	// has no filesystem access to find the primary binary). Empty when
	// RootConfigMessage is set (the common case — irrelevant) or when no
	// primary binary was found (a CLI/library project with no cmd tree
	// yet — config.go.tmpl then falls back to emitting no root alias at
	// all, matching the historical all-binary behavior for such
	// projects).
	PrimaryConfigMessage string

	// ProjectConfigPathEnv is the environment variable that names the
	// project-global config's file when --config is not given:
	// <PROJECT>_CONFIG (forgeconfig.ConfigPathEnvFor of the project name).
	// RootConfigMessage carries no (forge.v1.binary_config) annotation for the
	// runtime to derive a name from, so the generated RegisterFlags names it
	// on the --config flag. Set by GenerateConfigLoader.
	ProjectConfigPathEnv string

	// PrimaryConfigPathEnv is PrimaryConfigMessage's binary's
	// <BINARY>_CONFIG, for the generated doc comments. Set alongside
	// PrimaryConfigMessage.
	PrimaryConfigPathEnv 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

	// 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

	// EnvVarAliases are the further env vars the runtime reads for this
	// field when EnvVar is unset ((forge.v1.config).env_var_aliases), by
	// name. Read by validateConfigPathEnvs: an alias is read like EnvVar.
	EnvVarAliases []string
}

ConfigTemplateField holds template data for a single config field.

type ContradictoryOptionality added in v0.1.45

type ContradictoryOptionality struct {
	Message string // config message declaring the field
	Field   string // proto field name
	EnvVar  string // the field's env_var, when it declares one
}

ContradictoryOptionality names one field that claims to be both required and optional.

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 DeclaredWorkload added in v0.1.45

type DeclaredWorkload struct {
	Name     string
	BuildCmd string
	Args     []string
	// Serves lists the components this workload's process runs besides one
	// named for itself: the `serves` field, for a subcommand that mounts
	// several components (control-plane's `admin-api`).
	Serves []string
}

DeclaredWorkload is what one `<ident> = fw.Workload {...}` literal in deploy/kcl/workloads.k RUNS: its name, the cmd of its GoBuild (empty when it builds nothing forge compiles), its args, and the components it states it serves.

It is read from the source rather than evaluated, because its readers — `forge lint` and `forge scaffold` — must work on a checkout that has never rendered, and must not need kcl on PATH to do it.

func (DeclaredWorkload) ServesComponent added in v0.1.45

func (w DeclaredWorkload) ServesComponent(name string) bool

ServesComponent reports whether w lists the component named name in its `serves`. Names compare by KCL identifier, so `overview-admin` and `overview_admin` are the same component, as they are everywhere else in this file.

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 exported Go identifier of the synthesized stub
	// struct: "Stub" + the Deps field name (StubRepo). It is exported so
	// the external <pkg>_test package — where the scaffolded tests live —
	// can construct one with its Func fields set. Field names are unique
	// within one Deps, and each helper file is its own component's, so no
	// package prefix is needed to keep stubs apart.
	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. For a CrossPackage stub that includes the interface's
	// own package; for a LOCAL interface the interface type needs no
	// import but its parameter / result types still may, so this is
	// populated independently of CrossPackage. The bootstrap_testing assembler
	// deduplicates these across stubs into the top-level
	// ExtraImports field on the template data.
	ExtraImports []ExtraImport
	// Optional marks a `// forge:optional-dep` field. Its stub type is
	// emitted so a test can pass one through With<Svc>Deps, but
	// NewTest<Svc> does not install it: nil is the field's declared
	// state, and the code's nil path is what a test that says nothing
	// about the field should exercise.
	Optional bool
}

DepsAutoStub describes one synthesized interface implementation emitted into a service's generated helpers_gen_test.go for a service-owned Deps field. It exists so NewTest<Svc>(t) can construct the Service even when the field is required by validateDeps, and it is a programmable double shaped like a contract mock: a <Method>Func field per method, an embedded contractkit.Recorder, and a loud testkit.StubNotConfigured error from every error-returning method the test did not program (see programStubMethods).

type DepsField

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

	// Type is the pretty-printed Go type expression, e.g. "*slog.Logger",
	// "*config.Config", "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 DepsFuncDefault added in v0.1.45

type DepsFuncDefault struct {
	// FieldName is the Deps field name as declared (e.g. "Now", "NewID").
	FieldName string
	// Expr is the Go expression the harness assigns (e.g. "time.Now" or
	// the ULID id-generator closure).
	Expr string
	// NeedsTime / NeedsULID flag the import the Expr references so the
	// assembler can gate testing.go's `time` / ulid imports.
	NeedsTime bool
	NeedsULID bool
}

DepsFuncDefault describes one required func-typed Deps field the test harness fills with a real framework default, so NewTest<Svc> constructs the service without the author hand-rolling a func value (the Clock / IDGen friction). The template emits `if deps.<F> == nil { deps.<F> = <Expr> }` — matching the by-type Clock/IDGen fill compose.go emits on the live path, so tests and production share the same default.

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"`
	// Immutable marks a column declared `forge:immutable` in its catalog
	// comment (COMMENT ON COLUMN). It projects to Bun's `skipupdate`, so a
	// full-replace UPDATE leaves the stored value alone.
	Immutable bool `json:",omitempty"`
	// Version marks a column declared `forge:version` in its catalog
	// comment: the entity opts into optimistic concurrency control. See
	// forge/pkg/crud's meta.versionColumn for the write-path behavior.
	Version bool `json:",omitempty"`
	// FillStrategy carries a `forge:fill=<strategy>` declaration verbatim
	// ("ulid" or "handler"), "" when the column declares none. It names WHO
	// fills a column the wire never carries a value for — "ulid" makes the
	// ORM generate one at Create (see forge/pkg/crud's meta.fillULIDCols);
	// "handler" is pure acknowledgement that suppresses the
	// unsatisfiable-column lint and changes no codegen behavior. See
	// schemadef.ColumnMarkerFill for the full grammar and rationale.
	FillStrategy string `json:",omitempty"`
	// Owner marks a column declared `forge:owner` in its catalog comment:
	// the rows of this table belong to distinct principals, and this
	// column says whose. It injects no WHERE clause — only the
	// application knows how a caller's claims map onto these values — but
	// it names the column in the CRUD shim's scaffolded scoping
	// reminder, and it arms the `unscoped_auth` audit gate. See
	// schemadef.ColumnMarkerOwner.
	Owner bool `json:",omitempty"`
}

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

type EntityConstraint added in v0.1.45

type EntityConstraint struct {
	// Name is the identifier postgres reports in a violation's structured
	// constraint field — auto-derived (`jobs_estimate_id_key`) for an
	// inline declaration, explicit for a named one. It is carried verbatim
	// from the catalog: re-deriving postgres's naming rules in Go would be
	// forge grading its own homework against the only authority that
	// matters.
	Name string
	// Kind is "unique", "check" or "foreign_key" — see config.ConstraintKind.
	Kind string
	// Columns are the columns the constraint spans, in key order. Empty for
	// an expression-keyed unique index, whose key cannot be described by
	// bare columns without describing a stricter table than exists.
	Columns []string `json:",omitempty"`
}

EntityConstraint is one named constraint on an entity's table.

func EntityConstraintsFromTable added in v0.1.45

func EntityConstraintsFromTable(table schemadef.Table) []EntityConstraint

EntityConstraintsFromTable projects an introspected table's NAMED constraints — the ones a service branches on when a write comes back as a driver error.

The kinds are chosen by what a caller can DO with the violation, not by what the catalog happens to record:

  • UNIQUE (23505) is a lost race. It is the whole reason forge tells you to declare the constraint: the database is the only thing that can arbitrate two concurrent writes, and the loser maps to AlreadyExists.
  • CHECK (23514) is a declared domain invariant, so violating it is the caller's bad input and maps to InvalidArgument.
  • FOREIGN KEY (23503) means the parent row is absent or still referenced, which is a caller-visible NotFound / FailedPrecondition rather than a server fault.

The PRIMARY KEY is deliberately excluded. A duplicate-PK insert is not a domain event any service branches on — pkg/crud generates the id — so a generated constant for it would be noise in every entity file forever. NOT NULL is excluded because postgres names no constraint for it at all (it reports the column instead), so orm.ConstraintName returns "" and there is nothing to compare against.

A plain `CREATE UNIQUE INDEX` is included alongside a UNIQUE constraint even though the catalog does not consider it a constraint: postgres reports the INDEX name in the violation's constraint field, so from the error-handling side the two are indistinguishable, and omitting one would leave exactly the case that looks like the other unserved.

type EntityConvTemplateData

type EntityConvTemplateData struct {
	EntityName string // "Item" — the db.<Entity> struct
	// EntityPbType is the generated pb message type: EntityName in
	// protoc-gen-go's casing ("Oauth2token" → Oauth2Token).
	EntityPbType     string
	EntityLower      string   // "item"
	ToProtoAssigns   []string // statements: "m.Name = e.Name"
	FromProtoAssigns []string
}

EntityConvTemplateData renders one entity's conversion pair.

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"`
	// AppendOnly marks a table declared `forge:append-only` in its catalog
	// comment (COMMENT ON TABLE): an immutable ledger. Every generator that
	// emits a WRITE path must omit the mutating half for this entity —
	// notably the store interface, whose UpdateX/DeleteX otherwise
	// type-check against a table postgres will refuse at runtime.
	AppendOnly bool `json:",omitempty"`
	// Constraints are the table's named UNIQUE / CHECK / FOREIGN KEY
	// constraints. They project to per-entity name constants so a service
	// can branch on orm.ConstraintName(err) against something the
	// compiler checks. See EntityConstraintsFromTable for why the primary
	// key is not among them.
	Constraints []EntityConstraint `json:",omitempty"`
}

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 are conventions read off real columns (deleted_at, created_at+updated_at) — 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 FrontendEntities added in v0.1.45

func FrontendEntities(entities []EntityDef) (kept []EntityDef, dropped []string)

FrontendEntities is the subset of entities the frontend projections (CRUD pages, nav, mock fixtures, mock transport) can render coherently: those whose wire message carries a real key (EntityWireKey). The pages key rows, build detail/edit links and the mock store by that field, so an entity with no key on the wire (a surrogate `id` the message never exposes, served by a custom List+Update pair) would emit `item.id` against a message with no `id` — code that cannot typecheck. Such an entity stays schema and server CRUD; it just gets no generated UI. An entity with no wire fields at all (a legacy descriptor with no deep inventory) proves nothing and is kept. The second return names each dropped entity and why, for one-line reporting.

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.

Only MANAGED services' CRUD rpcs make an entity. A CRUD-shaped rpc on a `// forge:unmanaged` service is hand-implemented elsewhere; projecting an entity from it would wire generic CRUD, pages and seeds over business logic forge does not own. The entity appears when the service is adopted.

type EntityField

type EntityField struct {
	Name string // Proto field name: "patient_id"
	// GoName is the field on the GENERATED pb message ("PatientId";
	// "base64item" → "Base64Item") — naming.GoFieldNames. The db.<Entity>
	// struct spells the same column naming.ColumnGoName, which can differ.
	GoName    string
	ProtoType string    // "int64", "string", etc.
	GoType    string    // "int64", "string", etc.
	Kind      FieldKind // scalar, enum, message, etc.
	IsFK      bool
	FKTable   string // "patients" (if FK)
	// Optional is true when the wire field carries the explicit proto3
	// `optional` label. protobuf-go generates a POINTER struct field
	// (*string, *int64, ...) for explicit-presence scalars, so
	// conversion codegen must pair optional scalars like wrappers
	// (nil-safe pointer handling — see promoteOptionalScalar), never
	// like plain scalars. GoType/Kind here stay the PLAIN spelling; the
	// conversion layer promotes at its boundary so other consumers
	// (frontend projections) are untouched. Additive:
	// `json:",omitempty"` keeps old descriptors parseable.
	Optional bool `json:",omitempty"`
	// 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"`
	// Secret carries the field's `// forge:secret` marker through to the
	// conversion generator: a secret field is skipped in the toProto (read)
	// direction — never packed onto a Get/List response — while staying
	// settable on Create/Update. Additive (`json:",omitempty"`).
	Secret bool `json:",omitempty"`
	// ReadOnly carries the field's `// forge:read-only` marker: the field is
	// not client-writable, so the CRUD test's mutable/clobber-field
	// selection (and any other client-input projection) skips it. The value
	// stays on the entity + read responses; only the client-settable request
	// surfaces exclude it. Additive (`json:",omitempty"`).
	ReadOnly bool `json:",omitempty"`
	// MapValueKind is the proto kind of a map field's VALUE ("string",
	// "int64", "message", "enum", ...). GoType collapses every map to the
	// marker "map[string]string", so without this the conversion generator
	// cannot tell a map of scalars (which JSON round-trips as-is) from a
	// map of messages or enums (which do NOT — they would store protobuf
	// struct internals and enum wire NUMBERS respectively). Empty for
	// non-map fields. Additive (`json:",omitempty"`).
	MapValueKind string `json:",omitempty"`
}

EntityField represents a single field in an entity.

func WireEntityFields added in v0.1.45

func WireEntityFields(svc ServiceDef, entityName string) []EntityField

WireEntityFields extracts the entity wire-message fields from the service descriptor: the deep Schemas map when present, else the shallow Messages map (older descriptors). It is the WIRE half of an entity, the half BuildEntityConv pairs against the applied schema's columns — exported so the birth↔conversion contract can be asserted from internal/scaffold, which owns the other half.

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)
	// EnumType / EnumImportPath are set ONLY for real protobuf enum columns
	// (FieldKindEnum). protobuf-es enums are NUMBERS at runtime — a raw
	// item.status renders "2" — so the cell hands the enum OBJECT to
	// <StatusBadge value={item.field} enumType={EnumType}>, which resolves the
	// value to its member name once for BOTH the label and the colour. Empty
	// for enum-LIKE string columns (already a string) and non-enum columns.
	EnumType       string
	EnumImportPath string
	// IsMoney marks integer minor-unit (cents) columns — proto field name
	// ends in "_cents". The cell formats them as currency (formatMoneyCents)
	// instead of printing the raw bigint digits.
	IsMoney bool
	// CurrencyField is the camelCase name of a sibling ISO-4217 currency
	// field ("currency" / "currencyCode") on the SAME entity, set ONLY for
	// money columns whose entity carries one. The money cell passes it as
	// formatMoneyCents' currency arg so a JPY row renders "¥180", not
	// "$180". Empty → the entity has no currency field, so the cell keeps
	// the formatter's USD default.
	CurrencyField string
	// FK is non-nil when the column is a FOREIGN KEY onto another CRUD
	// entity. The DETAIL page then resolves it through <EntityName> — one
	// row, one lookup — instead of printing a UUID. The LIST page keeps
	// printing the raw id on purpose: resolving names per row costs a
	// request per distinct id, and the right fix there is a server-side
	// join the application owns. See AttachForeignKeys.
	FK *PageFieldFK
}

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

type EntityRPC added in v0.1.46

type EntityRPC struct {
	Service string
	Method  string
	At      string
}

EntityRPC is one declared rpc that makes a table an entity: unary, not `// forge:custom`, named Create<E>/Get<E>/Update<E>/Delete<E>/List<Es> on a service that declares the wire message <E>. At names it in the proto source ("proto/reliant/v1/message.proto:14"), or is "" when the line could not be found.

func (EntityRPC) String added in v0.1.46

func (r EntityRPC) String() string

type EnumGoRef added in v0.1.45

type EnumGoRef struct {
	ImportPath string // e.g. "github.com/acme/app/gen/controlplane/v1"
	GoName     string // protoc-gen-go identifier, e.g. "DaemonType" or "Order_Status"
}

EnumGoRef locates a generated Go enum type.

type EnvBindingResult added in v0.1.45

type EnvBindingResult struct {
	// Applied is true when a line was written.
	Applied bool
	// Binder is the binder of the line written.
	Binder string
	// Bound is what that line binds: `wl.<ident>` for the new workload, or
	// the env's own API workload (`_api`) when that is what runs it.
	Bound string
	// ServedBy names the env's own API workload when it is already bound and
	// already runs the new workload, so nothing was written.
	ServedBy string
}

EnvBindingResult is what AppendEnvBinding did to one env.

func AppendEnvBinding added in v0.1.45

func AppendEnvBinding(projectDir, projectName, env, kind, workloadName string) (EnvBindingResult, error)

AppendEnvBinding adds the binding for a new workload to one env's main.k, at the end of its `_workloads` list, and reports what it wrote and with which binder (envBinderIn: the way that env binds its workloads).

In an env that serves its API as one `server` workload (a hosted env's `_api`, see EnvServerWorkload), a new service or worker is already run by it, so it gets no line of its own — a second, separate workload would be one the browser cannot reach, or a worker run twice. If that API workload is not bound yet (a project born with no service), it is bound instead.

Like AppendWorkloadStanza it never rewrites what is there: nothing is written when the file is missing, already binds the workload (`wl.<ident>` anywhere in the list), or has been restructured past the point where the list is unambiguous. The caller prints the line.

type EnvVarAlias added in v0.1.45

type EnvVarAlias struct {
	Name       string
	Deprecated bool `json:",omitempty"`
}

EnvVarAlias is one (forge.v1.config).env_var_aliases entry.

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 FieldConstraints added in v0.1.45

type FieldConstraints struct {
	// Numeric bounds (apply to int*/uint*/sint*/fixed*/float/double).
	Gte string `json:"gte,omitempty"`
	Gt  string `json:"gt,omitempty"`
	Lte string `json:"lte,omitempty"`
	Lt  string `json:"lt,omitempty"`
	// String length in characters (runes).
	MinLen *uint64 `json:"min_len,omitempty"`
	MaxLen *uint64 `json:"max_len,omitempty"`
	// Pattern is an RE2 regular expression the whole string must match.
	Pattern string `json:"pattern,omitempty"`
	// Email requires a syntactically valid email address.
	Email bool `json:"email,omitempty"`
	// Required marks the field must-be-set (protovalidate `required`).
	Required bool `json:"required,omitempty"`
}

FieldConstraints holds the subset of buf.validate.field rules forge projects. Numeric bounds are kept as the raw literal token ("0", "100", "9.99") so an integer bound never picks up a spurious ".0" in the emitted SQL or zod. All fields are additive/omitempty so old descriptors (which carry none) round-trip unchanged.

func ParseRawValidateOptions added in v0.1.45

func ParseRawValidateOptions(optsText string) *FieldConstraints

ParseRawValidateOptions parses the protovalidate rules out of a field's inline options text (the `[...]` block, brackets optional) into a FieldConstraints, or nil when none of the supported rules appear. Both the dotted form (`.int64.gte = 1`) and the braced aggregate form (`.int64 = {gte: 1}` / `= {int64: {gte: 1}}`) are recognized, so a rule projects to the same CHECK no matter which spelling the author used. Rules outside the projected subset are ignored — they are still enforced at the wire by the interceptor.

func (*FieldConstraints) HasAny added in v0.1.45

func (c *FieldConstraints) HasAny() bool

HasAny reports whether any constraint is set — the guard every consumer uses before projecting.

func (*FieldConstraints) SQLChecks added in v0.1.45

func (c *FieldConstraints) SQLChecks(col, kind string) []string

SQLChecks returns the CHECK clause(s) to append to a column definition for a field of the given proto scalar kind. Each entry is a complete `CHECK (...)` expression. NULL rows pass every check by SQL three-valued logic, so nullable columns need no special form.

func (*FieldConstraints) SuppressesZeroDefault added in v0.1.45

func (c *FieldConstraints) SuppressesZeroDefault(kind string) bool

SuppressesZeroDefault reports whether a NOT NULL column carrying these constraints should OMIT its zero default. A constrained value is one the caller must supply — and its type's zero (empty string / 0) would often violate the very CHECK just emitted (min_len>=1, gte>0, a non-empty pattern, email). Dropping the default keeps the migration internally consistent and mirrors the existing *_id reference-column treatment (NOT NULL, no default — an empty reference is a bug, not a value).

func (*FieldConstraints) ZodChain added in v0.1.45

func (c *FieldConstraints) ZodChain(formType string) string

ZodChain returns the zod validator suffix (e.g. ".gte(0).lte(100)" or ".min(1).max(64).email()") for a form field of the given form type ("number" takes numeric bounds; everything else is treated as a string input). The base builder (z.coerce.number() / z.string()) is emitted by the template; this is only the trailing refinement chain.

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
)

The FieldKind values a proto field can be classified as. Kinds drive which conversion path the CRUD generator emits for the field.

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", "enum"
	FilterType string // "exact", "search"
	IsOptional bool   // proto optional keyword
	// IsEnum marks a pb-enum-typed filter field. Enum entity columns
	// store the proto enum VALUE NAMES as TEXT (CHECK (col IN (...))),
	// while the pb field is int32-kinded — binding the enum value raw
	// (`WhereEq("status", *req.Status)`) hit Postgres with `text =
	// integer` and errored on the first filtered call. The template binds
	// req.<F>.String() — the stored name — instead.
	//
	// UNSPECIFIED semantics: a PROVIDED enum filter always applies —
	// filtering by <ENUM>_UNSPECIFIED (0) matches the rows stored under
	// the UNSPECIFIED value name rather than being silently skipped
	// (skipping would make one declared value unfilterable). `optional`
	// on the request field is the way to express "no filter" (nil).
	IsEnum bool
	// EnumNumber marks an IsEnum filter whose column is an INTEGER, which
	// stores the wire number rather than the value name (a schema the author
	// declared — forge births TEXT). The template binds int32(req.<F>):
	// binding the name would hit Postgres with `integer = text`.
	EnumNumber bool
	// 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 FixtureConstraintError added in v0.1.45

type FixtureConstraintError struct {
	Violations []fixtureViolation
}

FixtureConstraintError reports that generated fixtures violate the applied schema's own CHECK constraints.

It is a distinct TYPE because its severity is distinct. Scaffolding the lifecycle test is otherwise best-effort — a project that cannot produce one still generates, and the failure is a warning — but a fixture the schema rejects is not an incidental miss. It is forge emitting a file it can prove wrong, and the whole point of checking is to refuse. Callers match on this type to fail the run while continuing to tolerate everything else.

func (*FixtureConstraintError) Error added in v0.1.45

func (e *FixtureConstraintError) Error() string

type ForgeDescriptor

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

	// SourceHash fingerprints the .proto files this descriptor was extracted
	// from. It exists because the descriptor is a DERIVED CACHE of state that
	// already lives in the protos, and a cache that cannot detect its own
	// staleness does not degrade — it LIES.
	//
	// Measured: renaming one RPC in the descriptor, touching no .proto, made
	// `forge project graph` report an RPC that existed in no proto file and
	// omit one that existed in six places. Nothing warned, because loading
	// was read-file-and-unmarshal with no validation of any kind.
	//
	// A missing descriptor was always handled gracefully; a WRONG one was
	// trusted completely. That asymmetry is backwards, and this field is what
	// inverts it — see DescriptorSourceHash and loadDescriptor.
	//
	// Empty means "written by a forge that predates the stamp": treated as
	// stale, because an unverifiable cache is exactly the thing being fixed.
	SourceHash string `json:"source_hash,omitempty"`
}

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

type FrontendConfig added in v0.1.45

type FrontendConfig struct {
	// Frontend is the forge.yaml frontends[].name this config belongs to.
	Frontend string
	// MessageName is the proto message carrying the annotation. It names
	// the generated KCL schema, so it appears in per-env config.k.
	MessageName string
	// Fields are the leaves the bundle reads, in proto declaration order:
	// own fields first, then each composed block's leaves.
	Fields []ConfigField
}

FrontendConfig is one frontend's resolved config surface: the message bound to it by (forge.v1.frontend_config).frontend, flattened to the leaf fields its bundle actually reads.

It is the browser twin of BinaryConfig and is built the same way — own leaves plus the leaves of every composed block — so a shared definition (the OIDC issuer both halves need) is declared once and projected into both the binary's env and the frontend's runtime config.

func FrontendConfigsFromMessages added in v0.1.45

func FrontendConfigsFromMessages(messages []ConfigMessage) []FrontendConfig

FrontendConfigsFromMessages collects every frontend-bound config message, flattening composed blocks exactly as ConfigFieldsForBinary does for a binary. Output is sorted by frontend name so generated artifacts are deterministic regardless of proto declaration order.

Callers MUST run ValidateFrontendConfigs first (or use it as a gate in the generate pipeline): this function does not itself refuse a sensitive field, because reporting every offender at once is more useful than failing on the first.

type FrontendHookMethod

type FrontendHookMethod struct {
	Name      string // PascalCase: "GetUser"
	NameCamel string // camelCase: "getUser" — forge's own query-key / hook spelling
	// ClientMethod is the method the connect-es client has for this RPC —
	// protobuf-es's localName (naming.EsMethodName), which lowers the
	// FIRST CHARACTER ONLY: "LLMChat" → client.lLMChat. NameCamel lowers
	// the whole leading acronym, so it names a method the client lacks.
	ClientMethod string
	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
	// InputSchemaRef / OutputTypeRef are the names the hooks file refers to
	// the request schema and response type by: their own, or
	// `<module>_<Name>` when the file imports that name from two modules
	// (tsImportPlan).
	InputSchemaRef string
	OutputTypeRef  string
	// contains filtered or unexported fields
}

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
	// ServiceRef is the name the file refers to the service descriptor by
	// (tsImportPlan): ServiceName unless another module the file imports
	// declares the same name.
	ServiceRef 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 project 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 GeneratedFieldMarker added in v0.1.45

type GeneratedFieldMarker struct {
	// Field is the proto field name the marker attached to.
	Field string
	// Expr is the SQL expression, verbatim ("" when the marker carried
	// none — birth refuses that).
	Expr string
	// Site locates the marker in its .proto file.
	Site RawProtoMarkerSite
}

GeneratedFieldMarker is one `// forge:generated <expr>` marker the raw scan bound to a field.

type GraphGenInput added in v0.1.46

type GraphGenInput struct {
	GenContext
	Services        []ServiceDef
	Packages        []BootstrapPackageData
	Workers         []BootstrapWorkerData
	Operators       []BootstrapOperatorData
	WebhookServices map[string]bool
	// Warnings receives advisory lines; nil is os.Stderr.
	Warnings io.Writer
}

GraphGenInput is GenerateGraph's input: the discovered component set.

type GuardedPageField added in v0.1.45

type GuardedPageField struct {
	// Name is the camelCase accessor the detail row reads off the entity.
	Name string
	// Label is the humanized column name ("Amount Paid Cents").
	Label string
	// ProtoName is the snake_case column — the update_mask path this field
	// is deliberately NOT contributing.
	ProtoName string
	// GuardedBy is the RPC that owns the column's writes ("RecordPayment").
	GuardedBy string
}

GuardedPageField is one column the scaffolded edit page refuses to write because a custom RPC owns it (`// forge:guards <table>.<column>`), plus the name of that RPC.

GuardedBy is the load-bearing half. The column could simply have been dropped — it is off the update_mask either way — but a field that is merely absent reproduces the discoverability failure the marker exists to close: the author who lowered an invoice's total below its collected amount and got a 500 learns nothing from a form control that is not there. A disabled row reading "managed by RecordPayment" is a pointer to the correct API at the one moment the user is looking for it, and it costs a row on a form that already shows every sibling column.

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 IgnoredResolverMarker added in v0.1.45

type IgnoredResolverMarker struct {
	File   string `json:"file"`
	Line   int    `json:"line"`
	Reason string `json:"reason"`
}

IgnoredResolverMarker is a ResolvesCallerDirective forge found and could not honour, with the reason — reported rather than dropped, so a marker that does nothing is visible.

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").
	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
	// Constructor is the component's entry-point func name — `New`, or the
	// `// forge:constructor`-marked name the author chose instead. Read
	// through [InjectComponentData.ConstructorName] so a literal built
	// without it still emits the canonical shape.
	Constructor string
	// Fallible reports whether the constructor returns (T, error). Build
	// wraps the error with the component name when true; assigns directly
	// otherwise.
	Fallible bool
	// Wrapped is true when the package declares the OWNED observability
	// wrapper (the generated middleware decorator — see observe_wrap.go) and the
	// constructor returns the contract interface. The construction is then
	// emitted as `c.X = pkg.<MiddlewareCtor>(pkg.New(...))` (or the fallible
	// two-step equivalent).
	Wrapped bool
	// MiddlewareCtor is the generated middleware wrapper's exact constructor
	// name (codegen.ResolveMiddlewareWrapper — keyed off the constructor's
	// concrete return type, e.g. "NewServiceWithForgeMiddleware"), so the emitted
	// `<Alias>.<MiddlewareCtor>(...)` call matches the generated declaration.
	// Only read when Wrapped is true.
	MiddlewareCtor string
	// Assignments are the per-Deps-field key/value pairs, in Deps
	// declaration order, for the New(Deps{...}) literal.
	Assignments []InjectAssignment
	// DepsKeys is every key the component's `Deps{…}` literal may legally
	// name, and DepsFound reports whether the Deps struct was positively read
	// (see DepsLiteralKeys). Template-invisible: only the reconciler's
	// stale-key drop reads them, and only when DepsFound is true.
	DepsKeys  map[string]bool
	DepsFound bool
}

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.

func (InjectComponentData) ConstructorName added in v0.1.45

func (d InjectComponentData) ConstructorName() string

ConstructorName is the constructor selector compose.go must emit for this component: the detected name, or the canonical `New` when the field was never populated. Templates call `{{.ConstructorName}}` so an unset field can never render an empty selector.

type InjectGenData

type InjectGenData struct {
	Module            string
	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
	// NeedsTime / NeedsULID gate the `time` and `github.com/oklog/ulid/v2`
	// imports for the framework Clock / IDGen seam. Only set when at least
	// one component's Deps carries a `func() time.Time` / `func() string`
	// field the seam fills, so a project that uses neither never imports
	// them (unused-import build failure).
	NeedsTime bool
	NeedsULID bool
	// NeedsDBStore gates the internal/db import for a generated-store dep.
	NeedsDBStore 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
	// ProjectDir is the project root. Template-invisible: the reconciler
	// reads it to find the Deps struct of a package compose.go still imports
	// but the discovered set no longer holds (composeDropStaleDepsKeys), and
	// to read that package's composition markers (composeStaleConstructions).
	// Empty disables both reads.
	ProjectDir string
	// Graph is set when the composition graph (GenerateGraph) owns
	// compose.go: a construction there is a new<X> method, not a Components
	// field, so composeStaleConstructions names that remedy instead.
	// Template-invisible.
	Graph bool
}

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

type InjectGenInput

type InjectGenInput struct {
	GenContext
	Services  []ServiceDef
	Packages  []BootstrapPackageData
	Workers   []BootstrapWorkerData
	Operators []BootstrapOperatorData
	// Graph assembles the components for the composition graph, whose
	// generated files name identifiers of their own.
	Graph bool
	// Warnings receives GenerateCompose's advisory lines — a construction
	// compose.go keeps that the project no longer composes. Nil means
	// os.Stderr.
	Warnings io.Writer
}

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, TYPES ONLY, e.g.
	// "context.Context, string". Empty when the method takes no
	// parameters.
	//
	// A stub's body never reads its parameters, so it does not name them.
	// Keeping the interface's names put them in one scope with everything
	// the body spells: an interface method has no body, so a parameter
	// there may be called `testkit`, `new`, a local type's name, or the
	// package its result type comes from — legal in the interface, a
	// build failure in the stub (`func (s) Get(user string) (user.Profile,
	// error) { return *new(user.Profile), nil }`).
	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 what the stub returns when the test has not
	// programmed the method: "return <zeroes>" as the builders render it,
	// or empty when the method returns nothing. programStubMethods then
	// replaces a trailing error's nil with testkit.StubNotConfigured.
	ReturnStatement string

	// ParamTypes is Params one entry per parameter (a variadic last one
	// spelled `...T`), and ResultTypes / ResultZeros are the result types
	// and the zero-value expression of each. The rendered strings above
	// cannot be split back apart — a func-typed parameter has commas of
	// its own — and the programmable stub needs each one: it names every
	// parameter to forward it, and swaps the trailing error's zero.
	ParamTypes  []string
	ResultTypes []string
	ResultZeros []string

	// The fields below are the programmable stub's rendering of the
	// method, filled by programStubMethods. Receiver and the parameter
	// names are chosen per method so neither can shadow a package the
	// body spells (see InterfaceMethod.Params).
	Receiver    string
	NamedParams string // "a0 context.Context, a1 ...string"
	CallArgs    string // "a0, a1..." — forwards to <Name>Func
	RecordArgs  string // "a0, a1"   — what Recorder.Record captures
	FuncType    string // "func(context.Context, ...string) error"
}

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

type InternalPackage added in v0.1.45

type InternalPackage struct {
	// Name is the module-relative path under internal/, in nested form:
	// "cache", "mcp/database".
	Name string
	// Dir is the package's on-disk directory.
	Dir string
	// Type is one of the InternalPackage* constants.
	Type string
	// Composition is one of the Composition* constants.
	Composition string
	// CompositionReason is the `//forge:not-a-component: <why>` text; empty
	// otherwise.
	CompositionReason string
}

InternalPackage is one discovered contract package under internal/.

func DiscoverInternalPackages added in v0.1.45

func DiscoverInternalPackages(projectDir string) ([]InternalPackage, error)

DiscoverInternalPackages walks internal/ and returns every directory holding a contract.go, in stable (walk) order.

Two opt-outs are honored, unioned, exactly as the bootstrap walk has always honored them: the forge.yaml `contracts.exclude` list (whole subtree) and a package's own `//forge:exclude-contract` directive (that package only — descendants may still be components). A testdata/ subtree is fixture code, never a package.

The forge.yaml read is best-effort: a project with no (or a broken) forge.yaml still has its packages discovered, it just has no excludes to apply. The command that loaded the config first has already reported the breakage.

A walk error is returned rather than swallowed so a caller emitting code can fail the pipeline instead of shipping a partial bootstrap, which surfaces much later as a mysterious "undefined: pkg" build error.

type Inventory added in v0.1.45

type Inventory []config.ComponentConfig

Inventory is a project's complete component set — Connect servers, workers, crons, operators and secondary binaries — read from the sources that DECLARE them. Code lives in code: there is no manifest and no forge.yaml block, so each kind is read from the artifact that IS its declaration.

server   → the proto descriptor            (IntrospectComponents)
worker   → internal/workers/<pkg>/         (DiscoverWorkerSpecs)
cron     → the same, carrying a Schedule const
operator → internal/operators/<pkg>/       (DiscoverOperatorSpecs)
          + APIGroup/APIVersion consts in the operator package
          + <lower>_controller.go per CRD
binary   → cmd/<pkg>/main.go, minus the primary binary

An Inventory is only ever produced by DiscoverProjectComponents. Nothing hands one out as a struct field, so "I have not looked yet" is not representable: a caller that wants to know what the project contains has to go and read the project.

Resolve components through the methods below rather than re-scanning the slice, so every consumer shares one matching rule (exact name; kind compared through EffectiveKind).

func DiscoverProjectComponents added in v0.1.45

func DiscoverProjectComponents(projectDir, primaryBinary string) Inventory

DiscoverProjectComponents reads the project at projectDir and returns its Inventory. primaryBinary is the cmd/<bin>/ leaf that hosts the project's main command tree (the forge.yaml project name); it is excluded from the binary-kind rows because it is the server binary, not an `scaffold binary` component.

ABSENCE IS AN ANSWER, and it is the only reason the result is ever empty: a project with no proto descriptor yet, no internal/workers/ and no secondary cmd/ trees genuinely has no components. There is no error return because there is no source whose failure would be indistinguishable from that — every input is a directory or descriptor that is either there and readable or legitimately not there yet.

func (Inventory) Named added in v0.1.45

func (inv Inventory) Named(name string) (config.ComponentConfig, bool)

Named returns the component claiming name, and whether one does. Components share a single name namespace across every kind.

func (Inventory) Names added in v0.1.45

func (inv Inventory) Names() []string

Names returns every component name, in inventory order.

func (Inventory) OfKind added in v0.1.45

func (inv Inventory) OfKind(kind string) Inventory

OfKind returns the components whose EffectiveKind is kind, in inventory order.

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_gen.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
	// MountMethod is the method this file calls to mount the service —
	// "Register" normally, "Mount" when the service declares an RPC of its
	// own called Register. One type cannot carry both a Register(mux,
	// opts...) helper and a Register(ctx, req) RPC, and the RPC wins.
	MountMethod 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.
	// ProtoServiceName is the RAW proto service name: protoc-gen-connect-go
	// spells the `<Service>Name` constant from it rather than from the
	// GoName (naming.ConnectServiceNameConst).
	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
	// Audience is the trust domain the service declares (`(forge.v1.service)
	// .audience`), "user" when it declares none: which route set mounts it.
	Audience string
}

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

	// MaxStacks is the parallel-dev-stack ceiling
	// (config.DevStackConfig.EffectiveMaxStacks). The fragment maps the
	// listener ports for EVERY reachable stack, not just the default one:
	// stack N binds listener port P at P + N*BlockSize, and k3d can only
	// forward a host port it mapped at cluster-CREATE time.
	//
	// Why this belongs here. The block-0 ports were derived from the render
	// while blocks 1..N-1 were a hand-maintained ports: list in the user's
	// k3d.yaml, under a comment that had to be updated by hand whenever the
	// ceiling moved. That is the same fact written in two places, and the two
	// silently disagreed: nothing checked the hand-written range against the
	// allocator, so a worktree past the last hand-mapped block allocated
	// cleanly and then failed as "gateway unreachable", nowhere near the
	// cause. Deriving the whole range from the ceiling makes the pre-map and
	// the allocator two views of ONE number.
	//
	// Zero or one means only the default stack — emit block 0 alone, which is
	// exactly the historical output.
	MaxStacks int

	// BlockSize is the port-number distance between adjacent stacks
	// (config.DevStackConfig.EffectiveBlockSize). Zero falls back to 100, the
	// value this arithmetic was hardcoded to before it was configurable.
	BlockSize int
}

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

type ListFilterField added in v0.1.45

type ListFilterField struct {
	Name       string // camelCase request field: "status", "active"
	NamePascal string // "Status", "Active" — the React setter is set<NamePascal>
	Label      string // display label: "Status", "Active"
	// Kind selects the control the template renders and how the value is
	// coerced into the request: "enum" (typed <select>), "bool" (tri-state
	// All/Yes/No <select>), "text" (string exact input), "number".
	Kind string
	// IsBigInt is true for a "number" filter over a 64-bit int field
	// (int64/uint64/…) — protobuf-es types those as bigint, so the request
	// value is BigInt(...) not Number(...).
	IsBigInt bool
	// EnumType / EnumImport / EnumValues are populated only for Kind "enum":
	// the protobuf-es TS enum identifier, the _pb module to import it from,
	// and the <option> refs/labels — the real members only, never the zero
	// value (see formEnumMeta.Values).
	EnumType   string
	EnumImport string
	EnumValues []PageEnumValue
}

ListFilterField is one discrete (non-search) server-side filter control projected from a List RPC request field. The born list page holds a piece of state per field and threads its value into the List call, so filtering is done by the backend query — never by fetching a page and filtering it in the browser.

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
	// Unresolved names each embedded interface whose methods could not be
	// read — `pkg.Store` whose package does not type-check, has an
	// unexported method, or an embed of a shape the parser does not know.
	// Methods is then missing what that embed contributes, and a stub
	// built from it would not compile, so the harness leaves such a field
	// to the test (computeAutoStubs reports it unresolved).
	Unresolved []string
	// ExtraImports lists every package the flattened method signatures
	// reference by qualifier, resolved through the import block of the
	// file each method was declared in.
	//
	// A LOCAL interface can still have CROSS-PACKAGE signatures — the
	// interface type itself is in this package, but its parameter and
	// result types need not be. control-plane's `proxy_authz` handler
	// is the canonical case:
	//
	//	type AccessDecider interface {
	//	    Authorize(ctx context.Context, in proxyauthz.Input) proxyauthz.Decision
	//	}
	//
	// The synthesized stub renders that signature verbatim, so the
	// generated helper file must import internal/proxyauthz or it does
	// not compile. Only qualifiers that actually appear in a rendered
	// signature are listed: blanket-importing the declaring file's whole
	// import block would produce UNUSED imports, which fails the build
	// just as hard as a missing one.
	ExtraImports []ExtraImport
}

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.

Only interfaces DECLARED in the package are listed (a Deps field typed `pkg.Iface` goes through ResolveCrossPkgInterface instead), but their method sets are complete: an embedded interface contributes its methods whether it is declared here, in another package (`worktreesweep.Store`, resolved through go/types), or is the predeclared `error`.

type LoginBrokerTemplateData added in v0.1.45

type LoginBrokerTemplateData struct {
	Module string
	// Name is the project's binary name, used in the error message that
	// tells an operator which command provisions the broker credential.
	Name string
	// MiddlewareDeclaresSessionCookie reports whether the project's OWN
	// pkg/middleware/middleware.go declares SessionCookieName — the one
	// name the broker sets and the auth interceptor reads. Every project
	// scaffolded since the interceptor learned the cookie does; an older
	// one keeps a local constant so this file still compiles against the
	// middleware it actually has.
	MiddlewareDeclaresSessionCookie bool
}

LoginBrokerTemplateData is the data shape for the owned internal/app/login_broker.go scaffold (app-login-broker.go.tmpl).

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 is (forge.v1.method).auth_required: true unless the
	// proto explicitly opts out, since the field is `optional bool` and
	// an unannotated RPC is taken to want a principal.
	//
	// It is INFORMATIONAL and FAILS OPEN. Nothing consults it at
	// runtime: it feeds `forge project map`/`graph`,
	// and it selects which sentence the scaffolded handler comment shows
	// the author. An RPC declaring auth_required: true and doing nothing
	// about it serves anonymous callers, which is what a measured run
	// found in 17 of 20 generated CRUD RPCs. Making it fail closed is an
	// interceptor decision, not a codegen one.
	AuthRequired 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
	// Custom reports a `// forge:custom` marker on the rpc
	// (ProtoMarkerCustom): its semantics are the author's, so it is a custom
	// rpc whatever its name — CRUDShape answers "" for it, and nothing CRUD
	// is projected from it. Absent on descriptors written before the marker.
	Custom bool `json:",omitempty"`
	// 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) GoName added in v0.1.45

func (m Method) GoName() string

GoName is the Go method protoc-gen-connect-go declares for this RPC on the service's Handler interface ("CreateOauth2token" → CreateOauth2Token). Name is the proto spelling; anything matched against Go source — a handler method, a stub, a mock — must use this.

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 mirrors Method.AuthRequired: true unless the proto
	// explicitly opts out. It carries into CRUDMethodTemplateData, where
	// it selects the handler comment's wording. It gates nothing.
	AuthRequired bool
}

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

func (MethodTemplateData) GoName added in v0.1.45

func (m MethodTemplateData) GoName() string

GoName is the method protoc-gen-connect-go declares for this RPC on the service's Handler interface — what a handler must name its method ("CreateOauth2token" → CreateOauth2Token). Name stays the proto spelling, which is what descriptor lookups and CRUD matching key on.

func (MethodTemplateData) InputGoName added in v0.1.45

func (m MethodTemplateData) InputGoName() string

InputGoName / OutputGoName are the pb Go types protoc-gen-go generates for the request and response messages, unqualified.

func (MethodTemplateData) OutputGoName added in v0.1.45

func (m MethodTemplateData) OutputGoName() string

OutputGoName — see InputGoName.

type MiddlewareWrapper added in v0.1.45

type MiddlewareWrapper struct {
	// Constructor is the exported wrapper constructor, e.g.
	// "NewServiceWithForgeMiddleware". Compose emits `<alias>.<Constructor>(…)`;
	// the generator declares `func <Constructor>(inner <Iface>) <Iface>`.
	Constructor string
	// Struct is the unexported decorator struct, e.g. "forgeMiddlewareService".
	Struct string
}

MiddlewareWrapper is the resolved identity of one component's generated middleware decorator.

func ResolveMiddlewareWrapper added in v0.1.45

func ResolveMiddlewareWrapper(dir, ifaceName string) MiddlewareWrapper

ResolveMiddlewareWrapper returns the generated middleware decorator's identity for the component package at dir whose constructor returns the wrapped interface ifaceName. This is what BOTH the generate walk (which passes the result into contract.WriteObservedDecorator) and the compose assembler call, so the emitted constructor call matches the generated declaration exactly.

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 into their own USER-OWNED per-RPC files — "scaffold and forget", not a forge-owned holding pen. If all methods are already implemented, it returns AllUpToDate=true.

One RPC, one file (RPCHandlerFileName) — see writeRPCHandlerStubs for the per-file write semantics and why the layout is the way it is.

If the user deletes an RPC's file, forge re-scaffolding that stub 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 handler files are deliberately NOT checksum-tracked. The per-RPC scaffold tests likewise record no checksum: they become 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"
	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, seed *SeedProjection) MockEntityTemplateData

EntityDefToMockData converts an EntityDef (parsed from proto) and its associated ServiceDef into MockEntityTemplateData for template rendering. seed is the project's dev dataset (nil when there is none) — see SeedProjection: every value below is the one the database holds for the same row.

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)

DetectDepsDBField records the call and delegates to DetectDepsDBFieldFunc when set.

func (*MockInspector) DetectFallibleConstructor

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

DetectFallibleConstructor records the call and delegates to DetectFallibleConstructorFunc when set.

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)

GetModulePath records the call and delegates to GetModulePathFunc when set.

func (*MockParser) ParseConfigProto

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

ParseConfigProto records the call and delegates to ParseConfigProtoFunc when set.

func (*MockParser) ParseConfigProtosFromDir

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

ParseConfigProtosFromDir records the call and delegates to ParseConfigProtosFromDirFunc when set.

func (*MockParser) ParseEntityProtos

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

ParseEntityProtos records the call and delegates to ParseEntityProtosFunc when set.

func (*MockParser) ParseServicesFromProtos

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

ParseServicesFromProtos records the call and delegates to ParseServicesFromProtosFunc when set.

type MockRecord

type MockRecord struct {
	Fields []MockFieldValue
}

MockRecord is a single mock object with field values.

type MockResult added in v0.1.46

type MockResult struct {
	// Wrote is true when the render landed: the mock is current.
	Wrote bool
	// Changed is true when the write changed the file's bytes (or
	// created it). Only a changed mock is news.
	Changed bool
	// Skipped, when Wrote is false, says why nothing was written, phrased
	// to follow "<file> not written — ".
	Skipped string
}

MockResult is what GenerateMock did, so generate's progress line can report it rather than what was asked: every re-run renders every mock, and almost all of them come out byte-identical.

func GenerateMock

func GenerateMock(svc ServiceDef, projectDir, mockDir string, cs *checksums.FileChecksums) (MockResult, error)

GenerateMock generates a mock file for a service. Services with zero RPCs are skipped — there is nothing to mock. The result says whether the mock was written, whether that changed its bytes, and why not when it was not.

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, string, *checksums.FileChecksums) (MockResult, error)
	GenerateCRUDHandlersFunc        func(ServiceDef, []CRUDMethod, string, string, *checksums.FileChecksums) (CRUDResult, error)
	GenerateCRUDTestsFunc           func(ServiceDef, []CRUDMethod, string, string, *checksums.FileChecksums) (CRUDResult, error)
	GenerateCmdServerFunc           func([]ConfigMessage, string, *checksums.FileChecksums) error
	GenerateCmdServerWithFieldsFunc func(map[string]bool, 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) GenerateBootstrapTesting

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

GenerateBootstrapTesting records the call and delegates to GenerateBootstrapTestingFunc when set.

func (*MockService) GenerateCRUDHandlers

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

GenerateCRUDHandlers records the call and delegates to GenerateCRUDHandlersFunc when set.

func (*MockService) GenerateCRUDTests

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

GenerateCRUDTests records the call and delegates to GenerateCRUDTestsFunc when set.

func (*MockService) GenerateCmdServer

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

GenerateCmdServer records the call and delegates to GenerateCmdServerFunc when set.

func (*MockService) GenerateCmdServerWithFields

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

GenerateCmdServerWithFields records the call and delegates to GenerateCmdServerWithFieldsFunc when set.

func (*MockService) GenerateConfigLoader

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

GenerateConfigLoader records the call and delegates to GenerateConfigLoaderFunc when set.

func (*MockService) GenerateMigrate

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

GenerateMigrate records the call and delegates to GenerateMigrateFunc when set.

func (*MockService) GenerateMissingHandlerStubs

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

GenerateMissingHandlerStubs records the call and delegates to GenerateMissingHandlerStubsFunc when set.

func (*MockService) GenerateMock

func (m *MockService) GenerateMock(svc ServiceDef, projectDir string, mockDir string, cs *checksums.FileChecksums) (MockResult, error)

GenerateMock records the call and delegates to GenerateMockFunc when set.

func (*MockService) GenerateServiceStub

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

GenerateServiceStub records the call and delegates to GenerateServiceStubFunc when set.

func (*MockService) RegenerateServiceFile

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

RegenerateServiceFile records the call and delegates to RegenerateServiceFileFunc when set.

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
	// RequestEntityFieldCamel is the request field an AIP-134 Create/Update
	// nests the entity under ({ base64item: {...}, updateMask }), as
	// protoc-gen-es names it: the update request's wrapper off the
	// descriptor, else the field the entity scaffolder writes.
	RequestEntityFieldCamel 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
	// UpdateResponseType / UpdateEntityFieldCamel are what the Update dispatch
	// row returns. It reuses Create's response shape when the service has a
	// Create RPC, and otherwise the Update RPC's own response — never an empty
	// name, which would render as the undefined import `Schema`.
	UpdateResponseType     string
	UpdateEntityFieldCamel string
	// contains filtered or unexported fields
}

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.

func (MockTransportEntity) CreateResponseSchema added in v0.1.45

func (e MockTransportEntity) CreateResponseSchema() string

CreateResponseSchema is the local name of the Create response's schema.

func (MockTransportEntity) EntitySchema added in v0.1.45

func (e MockTransportEntity) EntitySchema() string

EntitySchema is the local name of the entity message's own schema.

func (MockTransportEntity) GetResponseSchema added in v0.1.45

func (e MockTransportEntity) GetResponseSchema() string

GetResponseSchema is the local name of the Get response's schema.

func (MockTransportEntity) ListResponseSchema added in v0.1.45

func (e MockTransportEntity) ListResponseSchema() string

ListResponseSchema is the local name of the List response's schema.

func (MockTransportEntity) UpdateEntityField added in v0.1.45

func (e MockTransportEntity) UpdateEntityField() string

UpdateEntityField is the wrapping field on UpdateResponse, with the same fallback.

func (MockTransportEntity) UpdateResponse added in v0.1.45

func (e MockTransportEntity) UpdateResponse() string

UpdateResponse is the response message the Update dispatch row names: its own when the extraction resolved one, otherwise Create's (the historical shape). Never empty for an entity with an Update row — an empty name would render as the undefined import `Schema`.

func (MockTransportEntity) UpdateResponseSchema added in v0.1.45

func (e MockTransportEntity) UpdateResponseSchema() string

UpdateResponseSchema is the local name of the Update response's schema.

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.

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 NewMockTransportTemplateData added in v0.1.45

func NewMockTransportTemplateData(entities []MockTransportEntity) MockTransportTemplateData

NewMockTransportTemplateData is the mock-transport.ts data for entities: one merged import statement per proto module, and the name the file refers to each imported schema by. Each entity contributes the per-RPC schema set its dispatch row names (`{ListResponseType,GetResponseType,CreateResponseType}Schema` gated on `HasList`/`HasGet`/`HasCreate||HasUpdate`, plus the entity schema for the mutable store). Duplicate symbols within a module are collapsed, and both levels are sorted for deterministic output (import/order's alphabetize).

The entities belong to different services, whose protos are free to name a response message alike (`GetResponse`); such a schema is imported `as <module>_<Name>` from each module (tsImportPlan) and the entity's row names that — read through the entity's *Schema methods.

func (MockTransportTemplateData) FixtureEntities added in v0.1.45

func (d MockTransportTemplateData) FixtureEntities() []MockTransportEntity

FixtureEntities is one entity per fixture module (`@/mocks/<slug>_gen`), in first-seen order — what mock-transport.ts imports `* as <plural>Mocks`. An entity served by several services has a dispatch row per service but one fixture module, and importing it once per row declared the namespace twice (TS2300 Duplicate identifier 'thingsMocks').

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 OpenProcedureEntry added in v0.1.45

type OpenProcedureEntry struct {
	Const   string // "demov1connect.DemoServiceGetStatusProcedure"
	Service string // "demo.v1.DemoService"
	Method  string // "GetStatus"
}

OpenProcedureEntry is one RPC the project declared public.

type OpenProcedureImport added in v0.1.45

type OpenProcedureImport struct {
	Alias string // "demov1connect"
	Path  string // "example.com/app/gen/services/demo/v1/demov1connect"
}

OpenProcedureImport is one connect package the generated file imports.

type OpenProceduresTemplateData added in v0.1.45

type OpenProceduresTemplateData struct {
	Imports []OpenProcedureImport
	Open    []OpenProcedureEntry
}

OpenProceduresTemplateData drives middleware-procedures.go.tmpl.

func BuildOpenProcedures added in v0.1.45

func BuildOpenProcedures(services []ServiceDef, modulePath string) OpenProceduresTemplateData

BuildOpenProcedures projects the services' auth_required declarations onto the generated open-procedure set.

Only `auth_required: false` puts an RPC here. The descriptor defaults an unannotated RPC to true (fail-closed), so silence is never a way to open an endpoint — publishing one is an edit to the proto that a reviewer sees.

Streaming RPCs are included on the same terms as unary ones: the interceptor gates the procedure, and a stream is a procedure.

type OperatorSpec

type OperatorSpec struct {
	Name string
	Path string
}

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

func DiscoverOperatorSpecs added in v0.1.45

func DiscoverOperatorSpecs(projectDir string) []OperatorSpec

DiscoverOperatorSpecs is the operator-side analog of DiscoverWorkerSpecs, over internal/operators/.

type PageEnumImport added in v0.1.45

type PageEnumImport struct {
	Path  string   // TS module: "@/gen/services/brand/v1/brand_pb"
	Types []string // enum type identifiers, sorted: ["BrandStatus"]
}

PageEnumImport is one `import { A, B } from "path"` line a form page needs for its enum-typed fields.

type PageEnumValue added in v0.1.45

type PageEnumValue struct {
	// Ref is the full protobuf-es TS member reference the option's value
	// uses ("BrandStatus.ACTIVE"). Member names mirror protobuf-es's
	// local-name rule: the enum-name prefix is stripped from the proto
	// value name when every value carries it.
	Ref string
	// Label is the humanized display text ("Active", "In Review").
	Label string
}

PageEnumValue is one option of a generated enum <select>.

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 is true only when an explicit declaration says the field
	// cannot be empty: a protovalidate `required = true` or
	// `string.min_len >= 1` on the entity field (see requiredByRule), or a
	// non-optional foreign key, whose born column is `NOT NULL REFERENCES`
	// with no DEFAULT (see AttachForeignKeys). A NOT NULL column alone is
	// NOT a rule about input: `string notes` is born `NOT NULL DEFAULT ”`,
	// and the empty string is a value the API, the wire validator and the
	// database all accept.
	Required bool
	// Optional is the proto3 `optional` label: the field has presence, so
	// "absent" is a value the wire can carry and the born column is
	// nullable. An empty input for such a field submits as unset — see
	// EmptyIsUnset.
	Optional  bool
	ProtoType string // original proto type for reference
	// 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
	// TSType is the TypeScript type protobuf-es declares for this field's
	// ELEMENT ("string", "boolean", "number", "bigint", "Uint8Array"), from
	// the one projection in frontend_ts_scalars.go. Empty for enums,
	// timestamps and anything else with no scalar projection. It is what
	// ZodExpr / SubmitExpr / PrefillExpr are derived from, so a kind the
	// emitters have never seen cannot quietly be treated as a string.
	TSType string
	// ZodExpr is the whole zod schema expression for this field —
	// `z.coerce.number().gte(0)`, `z.string().min(1)`,
	// `z.string().regex(/^(-?\d+)?$/, "expected a whole number")`, or one
	// of those wrapped in the empty-as-unset preprocess (EmptyIsUnset).
	// Nothing is added at render time.
	//
	// It is one string rather than a branch chain in each template because
	// there are FOUR page templates (Next.js and Vite × create and edit)
	// and the chain has to agree with SubmitExpr in every one of them: the
	// zod line declares the type the submit expression consumes. Six
	// divergent copies of that agreement is how the form came to feed
	// number[] into bigint[].
	ZodExpr string
	// SubmitExpr is the TypeScript expression the submit handler assigns
	// to this field, over the form's `values`. Empty when the zod value IS
	// already the wire type (a plain string, a number, a checkbox boolean,
	// a typed enum select) and the `...values` spread carries it.
	SubmitExpr string
	// SubmitNote is a one-line comment the submit handler emits above
	// SubmitExpr when the conversion is not self-evident. Empty for none.
	SubmitNote string
	// PrefillExpr is the TypeScript expression the EDIT form's `values:`
	// block reads out of the fetched entity (`item`) to seed this input.
	// It is the inverse of SubmitExpr and must land on the zod schema's
	// type, not the wire type.
	PrefillExpr string
	// IsEnum marks fields that render as a typed <select> over the
	// enum's declared values (Type "select"). The zod schema validates
	// the coerced option value as an enum member
	// (z.coerce.number().pipe(z.nativeEnum(X))) so the submitted form
	// value IS the protobuf-es TS enum — spreading it into
	// mutation.mutate() satisfies MessageInit without casts. Enum fields
	// that can't be resolved to a typed select (cross-package enums, old
	// descriptors without the deep schema) are EXCLUDED from forms
	// entirely: the pre-fix z.string() text-input projection guaranteed
	// a frontend type error at the mutate() call.
	IsEnum bool
	// EnumType is the TS enum identifier ("BrandStatus", or
	// "Brand_Status" for an enum nested in a message).
	EnumType string
	// EnumValues are the select options in proto declaration order: the
	// REAL members, never the zero value (see formEnumMeta.Values).
	EnumValues []PageEnumValue
	// EnumZeroRef is the enum's zero member ("BrandStatus.UNSPECIFIED"),
	// which the zod schema refuses — the zod mirror of the born CHECK that
	// does not admit it. Empty when the enum declares no zero value.
	EnumZeroRef string
	// EnumImportPath is the _pb module declaring the enum; page-level
	// import lines are aggregated from it (Create/UpdateEnumImports).
	EnumImportPath string
	// ZodChain is the trailing zod validator chain projected from the
	// field's protovalidate rules (e.g. ".gte(0).lte(100)" or
	// ".min(2).max(64).email()"). Empty when the field carries no
	// projected rules. The form template appends it to the base builder
	// (z.coerce.number() / z.string()) — see the create/edit page
	// templates. The SAME rules are enforced on the wire by the
	// protovalidate interceptor.
	ZodChain string
	// FK is non-nil when this field is a FOREIGN KEY onto another CRUD
	// entity in the same project — `patient_id` on Order pointing at
	// Patient. The form then renders an <EntityPicker> over the referent's
	// generated List hook instead of a raw text input for a UUID. Resolved
	// by AttachForeignKeys; see frontend_pages_fk.go for the (deliberately
	// conservative) resolution rule.
	FK *PageFieldFK
	// contains filtered or unexported fields
}

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

func (PageField) EmptyIsUnset added in v0.1.45

func (f PageField) EmptyIsUnset() bool

EmptyIsUnset reports whether an EMPTY input for this field means "unset": the field is a proto3 `optional` that no rule makes required. Its zod schema then turns "" into undefined before validating, so the request omits the field — an empty `optional int32 year` is stored as NULL, not as year 0, and an empty optional email is not run through `.email()`.

Checkboxes, timestamps and repeated fields are excluded: a checkbox is never empty, and the other two already submit an empty input as absent.

func (PageField) EnumDefaultRef added in v0.1.45

func (f PageField) EnumDefaultRef() string

EnumDefaultRef is the option a CREATE form's NOT NULL enum select starts on: the first real member. It is the born column's DEFAULT (scaffold.bornEnumDefault — the first value the CHECK admits) and the value the generated write path stores when the field is omitted, so the form shows the value Create would store anyway instead of inventing a "pick one" rule the API does not have.

type PageFieldFK added in v0.1.45

type PageFieldFK struct {
	// EntityName is the referent's PascalCase entity name ("Patient").
	EntityName string
	// EntityLower is the lowercased singular ("patient"), used in
	// placeholder text ("Select a patient").
	EntityLower string
	// ListHook / GetHook are the referent's generated hook identifiers
	// ("useListPatients" / "useGetPatient").
	ListHook string
	GetHook  string
	// HooksModule is the TS module both hooks come from
	// ("@/hooks/admin-service-hooks"). It is often the SAME module as the
	// page's own hooks — CreateHookImports and friends merge them, because
	// two import statements for one module is an eslint error.
	HooksModule string
	// ItemsField is the camelCase repeated field on the referent's List
	// response ("patients") — what <EntityPicker itemsOf> reads.
	ItemsField string
	// NextTokenField is the referent's forward-cursor response field
	// ("nextPageToken"); empty when the List response declares none, in
	// which case the picker renders no "keep typing to narrow" hint.
	NextTokenField string
	// HasPageSize reports whether the referent's List request carries
	// page_size — the picker caps its popover page with it.
	HasPageSize bool
	// SearchFilterField is the referent's free-text List filter
	// ("search"/"query"/"q"). Empty means the referent's List RPC declares
	// no server-side text filter, so the picker CANNOT narrow: the emitted
	// call site turns the search box off rather than shipping one that
	// silently does nothing.
	SearchFilterField string
	// PkFieldCamel is the referent's primary key ("id") — the value a
	// picked option resolves to, and the Get request field.
	PkFieldCamel string
	// LabelField is the referent's human-readable display column
	// ("fullName"), reused from DisplayField. Empty when the entity has no
	// name/title-shaped field; the call site then falls back to the id and
	// carries a comment saying which prop to retarget.
	LabelField string
	// GetEntityField is the camelCase field the referent's Get response
	// wraps the entity in ("patient") — what <EntityName nameOf> reads.
	GetEntityField string
	// ResolveSelectedLabel asks the emitted picker for a <EntityName>
	// selectedLabel. Set on EDIT forms only: the value arrives from the
	// server, so the closed picker has no row to read a name from and would
	// otherwise display the raw UUID — the exact defect the picker exists to
	// remove. A CREATE form starts empty, so the same markup there would be
	// an import and a component that never render.
	ResolveSelectedLabel bool
}

PageFieldFK describes the entity a foreign-key field points at, in the terms the page templates need to render <EntityPicker> / <EntityName> against that entity's generated hooks.

type PageHookImport added in v0.1.45

type PageHookImport struct {
	Module  string
	Symbols []string
}

PageHookImport is one merged `import { a, b } from "module";` line a page emits for generated hooks. Merged because a page that references its own entity's hooks AND a referent's from the same generated module must emit exactly one import statement for it.

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
	// GuardedFields are the columns a custom RPC owns
	// (`// forge:guards <table>.<column>`), removed from UpdateFields and
	// rendered as disabled rows naming the RPC instead. They are NOT in
	// the update_mask — that is the point — and they are not in
	// CreateFields either, since a guard is a rule about transitions
	// rather than about the initial value.
	GuardedFields []GuardedPageField
	// HasGuardedFields gates the edit form's read-only guarded section.
	HasGuardedFields bool

	// 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
	// CreateEntityFieldCamel is the camelCase field the Create RESPONSE
	// wraps the new entity in ("thing" for `CreateThingResponse { Thing
	// thing = 1; }`), read off the descriptor. The create page uses it to
	// land on the record it just made. Empty when the response does not
	// carry the entity — the page then returns to the list, since there is
	// no id to navigate to.
	CreateEntityFieldCamel string
	// GetEntityFieldCamel is the TS property of the Get RESPONSE field that
	// carries the entity — what the detail and edit pages read the record
	// off (`data?.base64item`, via GetEntityAccessor). Read off the
	// descriptor, falling back to the scaffolded field name
	// (entityFieldCamel); never the entity name with its first letter
	// lowered, which protoc-gen-es does not generate for "Base64Item"
	// (base64item) or "LLMKey" (llmKey).
	GetEntityFieldCamel string
	// Response type names for imports
	ListResponseType   string // "ListTasksResponse"
	GetResponseType    string // "GetTaskResponse"
	CreateRequestType  string // "CreateTaskRequest"
	CreateResponseType string // "CreateTaskResponse"
	UpdateRequestType  string // "UpdateTaskRequest"
	UpdateResponseType string // "UpdateTaskResponse"
	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 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
	// ListColumns drives the LIST page's typed column array. It is Columns
	// minus the identity/audit fields — see listColumnExcluded for why a
	// table and a detail view want different sets.
	ListColumns []EntityPageField
	// SearchFields are the camelCase string-typed fields client-side
	// search filters over. Empty → the list page omits the search box.
	//
	// DEPRECATED for the list page: the born list page now filters
	// SERVER-SIDE via the List RPC's own filter fields (SearchFilterField /
	// ExactFilterFields below), so it no longer client-side-filters a single
	// fetched page. SearchFields is retained because DisplayField derivation
	// shares this pass and older tooling still reads it.
	SearchFields []string
	// DisplayField is the camelCase string field used as the human title.
	// See deriveDisplayField for the ranking and for what is deliberately
	// left out; empty when the entity carries no whole-label column, and
	// every reader then falls back to the primary key.
	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 / humanizeEnum imports in page
	// templates.
	HasBadgeColumns bool
	// HasMoneyColumns reports whether any column is an integer-cents money
	// field — gates the formatMoneyCents import in page templates.
	HasMoneyColumns bool
	// HasDateCreateFields / HasDateUpdateFields gate the timestamp
	// conversion imports (timestampFromDate / toDatetimeLocal) in the
	// create and edit form templates.
	HasDateCreateFields bool
	HasDateUpdateFields bool
	// CreateEnumImports / UpdateEnumImports are the `import { X } from
	// "path"` lines the create/edit form pages need for their enum-typed
	// fields (one entry per _pb module, type names deduped and sorted).
	// Precomputed here because the templates render each page's import
	// block once while the fields repeat per input.
	CreateEnumImports []PageEnumImport
	UpdateEnumImports []PageEnumImport

	// SearchFilterField is the camelCase List-request field wired to the
	// <Resource> debounced search box — a "search"/"query"/"q" filter the
	// backend turns into an ILIKE across the entity's string columns. Empty
	// when the List request declares no free-text search filter.
	SearchFilterField string
	// ExactFilterFields are the List request's discrete (non-search) filter
	// fields — enum, bool, or plain scalar — rendered as filter controls
	// that feed the List RPC server-side. Empty → the page renders no
	// discrete filter controls (it still paginates + shows the tristate).
	ExactFilterFields []ListFilterField
	// ListFilterEnumImports are the `import { X } from "path"` lines the enum
	// filter <select>s need (deduped + sorted, like Create/UpdateEnumImports).
	ListFilterEnumImports []PageEnumImport
	// HasScalarExactFilter is true when any ExactFilterField is a plain
	// text/number scalar — those inputs are debounced (useDebouncedValue), so
	// the list page imports the hook and holds a raw-input/debounced-value
	// pair per scalar filter.
	HasScalarExactFilter bool
	// ColumnEnumImports are the `import { X } from "path"` lines the enum
	// BADGE COLUMNS need, because the cell passes the enum OBJECT itself to
	// <StatusBadge enumType={EnumType}>. Used by the detail page and merged
	// into ListEnumImports for the list page. Deduped + sorted.
	ColumnEnumImports []PageEnumImport
	// ListEnumImports is the merged, deduped set of enum imports the LIST page
	// needs — filter <select>s (ListFilterEnumImports) plus badge columns
	// (ColumnEnumImports) — so an enum that is both a filter and a column is
	// imported exactly once (a duplicate import is a TS error).
	ListEnumImports []PageEnumImport
	// HasCursorPagination is true when the List REQUEST carries a page_token
	// field AND the List RESPONSE carries next_page_token — the cursor pair
	// the <Resource> Prev/Next footer walks. When false the page renders the
	// server's first/only page with no pager (graceful degrade).
	HasCursorPagination bool
	// HasPageSize is true when the List request carries a page_size field —
	// gates passing PAGE_SIZE in the request init.
	HasPageSize bool
	// NextTokenField is the camelCase response field holding the forward
	// cursor ("nextPageToken"); empty when the response declares none.
	NextTokenField string
	// HasTotalCount / TotalCountField expose the camelCase response count
	// field ("totalCount") — wave 4 populates it server-side — so the list
	// reports the real total instead of the current page's length.
	HasTotalCount   bool
	TotalCountField string
	// contains filtered or unexported fields
}

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.

func (PageTemplateData) CreateHookImports added in v0.1.45

func (p PageTemplateData) CreateHookImports() []PageHookImport

CreateHookImports returns the merged generated-hook imports the create page needs: its own Create hook plus the List hook of every FK field.

func (PageTemplateData) DetailHookImports added in v0.1.45

func (p PageTemplateData) DetailHookImports() []PageHookImport

DetailHookImports returns the merged generated-hook imports the detail page needs: its own Get (+ Delete) hooks plus each FK column's Get hook.

func (PageTemplateData) EditHookImports added in v0.1.45

func (p PageTemplateData) EditHookImports() []PageHookImport

EditHookImports returns the merged generated-hook imports the edit page needs: its own Get + Update hooks, plus each FK field's List hook (the picker) and Get hook (the closed picker's <EntityName> label).

func (PageTemplateData) EmitsDetailPage added in v0.1.45

func (p PageTemplateData) EmitsDetailPage() bool

EmitsDetailPage reports whether the detail page (`/<slug>/<id>`) is scaffolded. It is the ONE answer to "does a record have a page of its own": the page generator writes the detail route on it, and every other page that links to a record — the list's row click, the create page's landing — asks it too. A link built on any other condition is a 404 the moment the service has a List RPC but no Get RPC.

func (PageTemplateData) EmitsEditPage added in v0.1.45

func (p PageTemplateData) EmitsEditPage() bool

EmitsEditPage reports whether the edit page can be scaffolded. The page loads the record through the entity's Get hook before it renders the form, so an entity served by an Update RPC but no Get RPC has nothing to load from — the template would emit `use({ id })` with an empty hook name. No Get, no edit page.

func (PageTemplateData) GetEntityAccessor added in v0.1.45

func (p PageTemplateData) GetEntityAccessor() string

GetEntityAccessor is the property the detail and edit pages read the record off a Get response (`data?.<accessor>`): GetEntityFieldCamel, with getResponseEntityField's fallbacks for page data assembled without it.

func (PageTemplateData) HasCreateBytesFields added in v0.1.45

func (p PageTemplateData) HasCreateBytesFields() bool

HasCreateBytesFields / HasUpdateBytesFields report whether a form carries any `bytes`-typed field. A bytes column is edited as base64 — the encoding it already has on the wire — so those forms import protobuf-es's own base64 codec; every other form must not, or the pristine scaffold ships an unused import.

func (PageTemplateData) HasCreateFK added in v0.1.45

func (p PageTemplateData) HasCreateFK() bool

HasCreateFK reports whether the create form has a foreign-key field, which is what gates the <EntityPicker> / <EntityName> / Controller imports on the create page.

func (PageTemplateData) HasCreateRegistered added in v0.1.45

func (p PageTemplateData) HasCreateRegistered() bool

HasCreateRegistered reports whether the create form has any field driven by react-hook-form's register() — i.e. any NON-foreign-key field. Foreign keys go through <Controller>, so a form whose every field is an FK must not destructure `register` at all: an unused binding is an eslint error on a file forge just wrote.

func (PageTemplateData) HasDetailFK added in v0.1.45

func (p PageTemplateData) HasDetailFK() bool

HasDetailFK reports whether any listed column is a foreign key, which is what gates the <EntityName> import on the detail page.

func (PageTemplateData) HasURLFilters added in v0.1.45

func (p PageTemplateData) HasURLFilters() bool

HasURLFilters reports whether the list page has any filter control at all, and therefore whether it reads and writes the query string. False (an unfiltered list) leaves the page exactly as it was: no schema, no useSearchParams, and so no Suspense boundary either.

func (PageTemplateData) HasUpdateBytesFields added in v0.1.45

func (p PageTemplateData) HasUpdateBytesFields() bool

HasUpdateBytesFields is HasCreateBytesFields for the edit form.

func (PageTemplateData) HasUpdateFK added in v0.1.45

func (p PageTemplateData) HasUpdateFK() bool

HasUpdateFK is HasCreateFK for the update form.

func (PageTemplateData) HasUpdateRegistered added in v0.1.45

func (p PageTemplateData) HasUpdateRegistered() bool

HasUpdateRegistered is HasCreateRegistered for the update form.

func (PageTemplateData) ListReactImports added in v0.1.45

func (p PageTemplateData) ListReactImports() []string

ListReactImports is the `react` import list the list page needs, in eslint's canonical (alphabetical) order. Computed rather than spelled out in the template because the conditions overlap — a page with both a cursor and a scalar filter needs useState once, not twice, and a duplicate specifier is a TS error on a file forge just wrote.

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 ProcessSelector added in v0.1.45

type ProcessSelector struct {
	Binary   string
	Root     *BinaryConfigFields
	Commands []BinaryConfigFields
}

ProcessSelector is one binary that scopes config to its subcommands: its root config (the binary-wide surface, nil when it declares none) and its command configs, ordered LONGEST command first so the first prefix match is the most specific one.

It exists because, for such a binary, no single per-message env map is a workload's env: the process running `reliant server api` reads the root config AND the api config. The generated selector composes the two, picks the command config from the workload's own argv — so which workload gets which env is derived from the command it runs, not hand-filtered per workload — and splits the workload's config_secrets between them.

func ProcessSelectorsFrom added in v0.1.45

func ProcessSelectorsFrom(perBinary []BinaryConfigFields) []ProcessSelector

ProcessSelectorsFrom groups per-binary configs into one ProcessSelector per binary that declares at least one command config, in first-declaration order. A binary with only a root config needs no selector: its one env map is the process's env.

func (ProcessSelector) LambdaName added in v0.1.45

func (ps ProcessSelector) LambdaName() string

LambdaName is the generated selector: `reliant` -> `reliantProcessEnvMap`.

func (ProcessSelector) SchemaName added in v0.1.45

func (ps ProcessSelector) SchemaName() string

SchemaName is the generated KCL schema bundling the binary's config instances: `reliant` -> `ReliantProcess`.

type RawProtoMarkerSite added in v0.1.45

type RawProtoMarkerSite struct {
	// File is the absolute path of the declaring .proto file.
	File string
	// Line is the 1-based line number of the marker.
	Line int
	// Text is the marker's source line, trimmed.
	Text string
}

RawProtoMarkerSite locates one `// forge:*` marker in a source file.

func (RawProtoMarkerSite) String added in v0.1.45

func (s RawProtoMarkerSite) String() string

String renders the site as `<file>:<line>: <source line>`.

type RawProtoMessage added in v0.1.45

type RawProtoMessage struct {
	// Name is the message's relative name within the package ("Order").
	Name string
	// File is the absolute path of the declaring .proto file.
	File string
	// Package is the file's proto package.
	Package string
	// Marked reports whether ANY birth marker (`// forge:entity`,
	// `// forge:append-only`, `// forge:soft-delete`) precedes the
	// declaration — all three tablize the message.
	Marked bool
	// AppendOnly reports a `// forge:append-only` marker: the entity is
	// born without Update/Delete RPCs and with a DB guard rejecting
	// UPDATE/DELETE.
	AppendOnly bool
	// SoftDeleteMarked reports a `// forge:soft-delete` marker: the birth
	// migration adds a nullable deleted_at column (opt-in soft delete).
	SoftDeleteMarked bool
	// Fields are the message's top-level fields in declaration order,
	// classified into the same SchemaFieldDef shape the compiled
	// descriptor uses (fully-qualified TypeNames, oneof membership, map
	// kinds) so downstream renderers consume one vocabulary regardless
	// of which truth supplied the fields.
	Fields []SchemaFieldDef
	// BodyOpen / BodyClose are byte offsets into the declaring file: the
	// position just AFTER the message's opening `{`, and the position OF
	// its matching `}`. They exist so a birth-time editor can append
	// fields to the message the author wrote without reformatting it
	// (stripBlockComments preserves length, so these index the raw file
	// bytes too).
	BodyOpen  int
	BodyClose int
	// MaxFieldNumber is the highest field number declared directly in this
	// message — including `reserved` numbers, and the fields of any oneof
	// declared in it (one number space), but NOT nested messages/enums
	// (their own space). An appended field takes MaxFieldNumber+1 and can
	// therefore never collide with, or renumber, what the author wrote.
	MaxFieldNumber int
	// UnappliedReadOnlyMarkers are read-only-family field markers
	// (ReadOnlyProtoMarkers: `// forge:read-only`, `// forge:computed`,
	// `// forge:generated`) written inside this message that attached to NO
	// captured field. A marker the scanner cannot honour must never be
	// dropped in silence — the author believes the field is off the write
	// surface and it would ship on the born Create request — so birth
	// refuses the entity and names the site.
	UnappliedReadOnlyMarkers []RawProtoMarkerSite
	// GeneratedMarkers are the `// forge:generated <expr>` markers bound to
	// a field of this message, one per marker, in declaration order. The
	// expression also lands on the field (SchemaFieldDef.Generated); this
	// ledger adds the SITE, so a birth that refuses the expression — empty,
	// on a field with no single column, or rejected by postgres — names the
	// file and line the author has to fix.
	GeneratedMarkers []GeneratedFieldMarker
	// RetiredMarkers are `forge:*` spellings written inside this message
	// that forge USED to recognize and deliberately removed
	// (RemovedProtoMarkers) — today they read as ordinary prose and do
	// nothing at all.
	//
	// This is the ledger UnappliedReadOnlyMarkers structurally cannot
	// populate: that one holds markers forge RECOGNIZES but could not
	// place, and a retired spelling is recognized by nothing. Without this
	// field the most dangerous case — `// forge:server-set`, whose author
	// believes a field is off the write surface — exits zero in silence.
	RetiredMarkers []RetiredProtoMarkerSite
}

RawProtoMessage is one TOP-LEVEL message captured by the raw scan.

type RawProtoRPC added in v0.1.45

type RawProtoRPC struct {
	Name string
	// File is the absolute path of the declaring .proto file.
	File string
	// Service is the name of the service block that encloses the rpc, or ""
	// when the scan could not attribute it to one.
	Service string
	// Request is the request message type as written in the signature,
	// minus any `stream` keyword and leading dot ("ListOrdersRequest",
	// "shared.v1.PageQuery"). Not resolved against the declared types.
	Request string
	// Streaming reports a `stream` keyword on either side.
	Streaming bool
	// Custom reports a `// forge:custom` marker above the rpc
	// (ProtoMarkerCustom) — the raw-source reading of Method.Custom
	// (rawRPCMarkerComments). Generate refuses a project where the two
	// disagree, so after a successful generate they are the same answer.
	Custom bool
}

RawProtoRPC is one rpc declaration captured by the raw scan.

type RawProtoScan added in v0.1.45

type RawProtoScan struct {
	// Package is the proto package shared by the scanned files.
	Package string
	// ServiceName / ServiceFile identify the first `service X {` block
	// found (the injection target for quintet completion). Empty when no
	// service block exists in the directory.
	ServiceName string
	ServiceFile string
	// Services are every service block found, in file+declaration order.
	Services []RawProtoService
	// Files are the absolute paths scanned, sorted.
	Files []string
	// Messages are the top-level messages, in file+declaration order.
	Messages []RawProtoMessage
	// Enums maps fully-qualified enum names (including message-nested
	// ones, "pkg.Order.Status") to their value names in declaration
	// order — the CHECK-constraint vocabulary for entity births.
	Enums map[string][]string
	// RPCs are every rpc declaration found across the files.
	RPCs []RawProtoRPC
}

RawProtoScan is the raw-truth view of one service's proto directory.

func ScanRawProtoDir added in v0.1.45

func ScanRawProtoDir(dir string) (*RawProtoScan, error)

ScanRawProtoDir scans every .proto file under dir (recursively) and returns the raw-truth view. A missing directory returns an empty scan, not an error — callers treat "nothing there" as "nothing to do".

func ScanRawProtoFiles added in v0.1.45

func ScanRawProtoFiles(files []string) (*RawProtoScan, error)

ScanRawProtoFiles scans exactly the given .proto files and returns the raw-truth view. The files are taken to share one proto package (the scan qualifies every type name with the first `package` it reads), so a caller covering a whole tree hands it one package directory's files at a time rather than the recursive set ScanRawProtoDir collects.

func (*RawProtoScan) DeclaresRPC added in v0.1.45

func (s *RawProtoScan) DeclaresRPC(name string) bool

DeclaresRPC reports whether any scanned file declares rpc <name>.

func (*RawProtoScan) MessageByName added in v0.1.45

func (s *RawProtoScan) MessageByName(name string) (RawProtoMessage, bool)

MessageByName returns the top-level message with the given relative name, if scanned.

func (*RawProtoScan) RPCNames added in v0.1.45

func (s *RawProtoScan) RPCNames() map[string]bool

RPCNames returns the set of rpc names declared across the scanned files.

func (*RawProtoScan) ServiceUnmanaged added in v0.1.45

func (s *RawProtoScan) ServiceUnmanaged(name string) bool

ServiceUnmanaged reports whether the named service carries a `// forge:unmanaged` marker. False for a service the scan did not find.

type RawProtoService added in v0.1.45

type RawProtoService struct {
	Name string
	// File is the absolute path of the declaring .proto file.
	File string
	// Unmanaged reports a `// forge:unmanaged` marker above the declaration
	// (ProtoMarkerUnmanaged) — the raw-source reading of
	// ServiceDef.Unmanaged, for passes that run before or without the
	// compiled descriptor.
	Unmanaged bool
	// BodyOpen / BodyClose are byte offsets into File: just after the
	// opening `{`, and at the matching `}`. Zero when the body was not
	// found (a single-line `service X {}` still records both).
	BodyOpen  int
	BodyClose int
}

RawProtoService is one `service X {` block captured by the raw scan.

type ReasonedMarker added in v0.1.45

type ReasonedMarker struct {
	// File is the absolute path of the file carrying the marker.
	File string
	// Line is the marker's 1-indexed line.
	Line int
	// Reason is the text after `forge:<directive>:`; "" for a bare marker.
	Reason string
}

ReasonedMarker is one occurrence of a package directive that takes a reason — `//forge:exclude-contract: <why>` or `//forge:not-a-component: <why>`.

func FindExcludeContractMarker added in v0.1.45

func FindExcludeContractMarker(dir string) (ReasonedMarker, bool)

FindExcludeContractMarker returns the package's `//forge:exclude-contract` marker, scanning non-test, non-generated files in name order so the answer is deterministic. When a package carries several, the first REASONED one wins — a package that already says why is not made to say it twice.

func FindExcludeContractMarkers added in v0.1.46

func FindExcludeContractMarkers(dir string) []ReasonedMarker

FindExcludeContractMarkers returns EVERY `//forge:exclude-contract` marker in the package, in file-name then line order. A package directive is a property of the package, like its package clause: it applies from whichever file declares it, and a package that declares it twice must say the same thing both times — the lint gate (internal/linter/contract) refuses two declarations that disagree.

func FindNotAComponentMarker added in v0.1.45

func FindNotAComponentMarker(dir string) (ReasonedMarker, bool)

FindNotAComponentMarker returns the package's `//forge:not-a-component` marker, preferring a reasoned one (see FindExcludeContractMarker).

func FindNotAComponentMarkers added in v0.1.46

func FindNotAComponentMarkers(dir string) []ReasonedMarker

FindNotAComponentMarkers returns every `//forge:not-a-component` marker in the package (see FindExcludeContractMarkers).

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 RetiredProtoMarkerMatch added in v0.1.45

type RetiredProtoMarkerMatch struct {
	// Offset is the byte offset of the match within the scanned content.
	Offset int
	// Marker is the retired spelling as registered (e.g. forge:server-set).
	Marker string
	// ReplacedBy is the marker that superseded it.
	ReplacedBy string
}

RetiredProtoMarkerMatch is one retired-spelling occurrence located in proto source, before it is attributed to a message.

func RetiredProtoMarkerSites added in v0.1.45

func RetiredProtoMarkerSites(content string) []RetiredProtoMarkerMatch

RetiredProtoMarkerSites returns every retired-spelling occurrence in one .proto file's source, as byte offsets paired with the marker found and its replacement.

This is deliberately a SOURCE scan over the same text the raw scanner reads, not a token scan over all prose containing `forge:`: only the exact retired spellings in RemovedProtoMarkers can match, so a comment merely mentioning `forge:server-set` in a sentence still matches (it is the same regex that would have honoured it) while unrelated prose, unknown typos, and markers belonging to other passes (forge:mutation, forge:secret) cannot. Typos stay the lint check's business at warning severity; a retired spelling has a definite fix and is escalated here.

type RetiredProtoMarkerSite added in v0.1.45

type RetiredProtoMarkerSite struct {
	// File is the absolute path of the declaring .proto file.
	File string
	// Line is the 1-based line number of the marker.
	Line int
	// Text is the marker's source line, trimmed.
	Text string
	// Marker is the retired spelling found (e.g. forge:server-set).
	Marker string
	// ReplacedBy is the marker that superseded it (e.g. forge:read-only).
	ReplacedBy string
}

RetiredProtoMarkerSite locates one retired `forge:*` spelling and carries the marker that replaced it, so the refusal can name the fix rather than only the problem.

func (RetiredProtoMarkerSite) String added in v0.1.45

func (s RetiredProtoMarkerSite) String() string

String renders the site as `<file>:<line>: <source line>`, matching RawProtoMarkerSite so both ledgers read alike in a refusal.

type ScenarioRPCData added in v0.1.45

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 added in v0.1.45

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 added in v0.1.45

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"`
	// Validate carries the field's protovalidate (buf.validate.field)
	// rules, projected to a DB CHECK at entity birth and a zod validator
	// in the generated form (the wire is enforced by the interceptor).
	// nil when the field carries no rules. Populated by both extractors —
	// the compiled descriptor and the raw proto scan.
	Validate *FieldConstraints `json:"validate,omitempty"`
	// ValidateOptions is the field's `(buf.validate.field)` inline-options
	// block exactly as authored, collapsed to one line
	// (`[(buf.validate.field).string.min_len = 1]`), or "" when the field
	// carries no protovalidate rule. Unlike Validate — the lossy subset
	// forge projects to SQL/zod — this is the whole declaration, so a
	// request message that FLATTENS this field (Create<Entity>Request) can
	// re-declare the SAME rules and have the wire interceptor enforce them.
	// Populated by the raw proto scan, which reads the authored spelling off
	// the file the injection edits. `json:",omitempty"` keeps old
	// descriptors parseable (additive contract).
	ValidateOptions string `json:"validate_options,omitempty"`
	// Secret marks a field carrying the `// forge:secret` leading-comment
	// marker: the column is real (schema truth), but the generated
	// proto←entity conversion (the toProto read path) NEVER packs it, so
	// Get/List responses omit it — while Create/Update stay free to set it.
	// Read off the compiled descriptor's source-code comments (the raw
	// proto scan does not need it — birth stores the column unconditionally).
	// `json:",omitempty"` keeps old descriptors parseable (additive contract).
	Secret bool `json:"secret,omitempty"`
	// ReadOnly marks a field carrying the `// forge:read-only` marker: the
	// INPUT-side mirror of Secret. The column is real and stays on the entity
	// message + Get/List responses (readable), but the field is OMITTED from
	// the generated Create/Update REQUEST messages — a value (status,
	// computed price, lifecycle timestamp) the client must not write. Read
	// off BOTH extractors (the compiled descriptor's source-code comments AND
	// the raw proto scan — a brand-new `// forge:entity` message births its
	// quintet from the raw scan, before the descriptor knows it).
	// `json:",omitempty"` keeps old descriptors parseable (additive contract).
	ReadOnly bool `json:"read_only,omitempty"`
	// Generated is the SQL expression a `// forge:generated <expr>` marker
	// carries, verbatim (see codegen.ProtoMarkerGenerated), or "" when the
	// field has none. Birth emits the column as `GENERATED ALWAYS AS
	// (<expr>) STORED`; the marker also sets ReadOnly, so the field leaves
	// the born write envelopes. Nothing after birth reads it — the applied
	// schema's GENERATED column is the truth from then on.
	//
	// Read off BOTH extractors, like ReadOnly: the raw scan for a
	// brand-new `// forge:entity` message, and the compiled descriptor for
	// `forge scaffold entity --from-proto <svc>.<Message>`, which would
	// otherwise see a read-only field and birth a plain column nothing ever
	// writes. `json:",omitempty"` keeps old descriptors parseable.
	Generated string `json:"generated,omitempty"`
	// Guards are the `<table>.<column>` targets this field declares with
	// `// forge:guards` — the columns whose writes the ENCLOSING RPC owns.
	//
	// Unlike every other flag here, this one is not a statement about the
	// field it sits on. The field is the attachment point; the subject is
	// a column on some entity, often on a different message entirely
	// (RecordPaymentRequest.amount_cents guards
	// invoices.amount_paid_cents). That indirection is the whole reason
	// the marker names its target instead of being inferred — see
	// ProtoMarkerGuards for the pair of columns that makes name-matching
	// unsound.
	//
	// Read off the compiled descriptor's source-code comments. The raw
	// proto scan does not need it: a guard changes no born schema, only
	// which columns the scaffolded edit page is willing to write.
	// `json:",omitempty"` keeps old descriptors parseable (additive
	// contract).
	Guards []string `json:"guards,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 SeedProjection added in v0.1.45

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

SeedProjection is the project's dev dataset as the frontend mock generator reads it: the same seedplan.Plan `forge db seed apply` renders, queried per cell instead of rendered as SQL.

A nil *SeedProjection is valid everywhere and means "no dataset to agree with" — no migrations, no reachable shadow server, or a schema the planner refuses (a NOT NULL foreign-key cycle). Every value then falls back to a type-correct, self-evidently synthetic literal, which is exactly what the seeder would have written for a column nothing describes.

func BuildSeedProjection added in v0.1.45

func BuildSeedProjection(projectDir string, cfg seedplan.Config) (*SeedProjection, error)

BuildSeedProjection resolves the project's seed plan from its migrations. cfg is the project's own seed configuration — the salt and row counts the dev dataset is built with — passed in rather than re-derived here so the mocks and `forge db seed apply` cannot be looking at different plans.

cfg.Now is IGNORED: the projection always anchors relative time at seedplan's fixed reference instant. Everything rendered from it is committed generated code, which must be a pure function of its inputs; a seeded timestamp that followed the caller's clock rewrote every fixture at midnight UTC. See newSeedProjection.

func (*SeedProjection) Rows added in v0.1.45

func (p *SeedProjection) Rows(table string) int

Rows is how many rows the dataset holds for a table. 0 means "no plan", and the caller keeps its own default.

func (*SeedProjection) Value added in v0.1.45

func (p *SeedProjection) Value(table, column string, row int) (string, bool)

Value returns the value the seeded database holds at (table, column, row), raw and unquoted. ok is false for a cell with no plain scalar spelling — NULL, an array, a boolean — and for every cell when there is no plan.

type SensitiveFrontendField added in v0.1.45

type SensitiveFrontendField struct {
	Frontend    string // forge.yaml frontend name
	ConfigName  string // the frontend-bound message
	DeclaredOn  string // message the field is declared on (block or config)
	FieldName   string // proto field name
	ViaBlock    string // composing field name when reached through a block
	EnvVarOrNil string // the field's env_var, when it declares one
}

SensitiveFrontendField names one refusal: a field marked sensitive that reaches a frontend, and the message it was declared on (which may be a shared block rather than the annotated message itself — naming the DECLARATION site is what makes the error actionable).

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, projectDir, mockDir string, cs *checksums.FileChecksums) (MockResult, error)

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

	// Config loader / cmd-server wiring.
	GenerateCmdServer(messages []ConfigMessage, targetDir string, cs *checksums.FileChecksums) error
	GenerateCmdServerWithFields(configFields map[string]bool, 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.

forge:no-observe Pure compute: empty Deps, and the work is template rendering and file emission inside a CLI process. There is no request lifecycle to correlate spans against and no remote call to time.

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" → [...])

	// Audience is the trust domain the service serves, from
	// `option (forge.v1.service) = {audience: "..."}`: "user" when the
	// service declares none. A serverkit.Listener mounts the service only
	// on a chain whose Authenticator admits this audience.
	Audience string `json:",omitempty"`

	// Schemas is the deep type graph for full JSON-Schema emission:
	// 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"`

	// EnumNumbers maps the same fully-qualified enum names to each value's
	// DECLARED wire number, parallel to Enums (EnumNumbers[fq][i] is the
	// number of Enums[fq][i]). Declaration order is not the wire number once
	// an enum reserves a removed value (`reserved 1;`), and protobuf-es types
	// an enum field as its declared numbers, so anything that emits an enum
	// as a number — the frontend mock fixtures — must read it from here.
	// Absent on descriptors written before this field existed; readers then
	// fall back to declaration order.
	EnumNumbers map[string][]int32 `json:",omitempty"`

	// EnumGoRefs maps the same fully-qualified enum names to where
	// protoc-gen-go put the enum: the Go import path of its declaring
	// file's go_package and its Go identifier. The proto package says
	// nothing about this — an enum in a sibling file with another
	// go_package shares the proto package but not the Go package, and its
	// value constants are not reachable through the service's `pb` import.
	// Absent on older descriptors; readers then fall back to the proto
	// package rule (enumWireGoName).
	EnumGoRefs map[string]EnumGoRef `json:",omitempty"`

	// Unmanaged reports a `// forge:unmanaged` marker above the service
	// declaration (ProtoMarkerUnmanaged): its handlers live outside forge's
	// model, so nothing service-scoped is generated for it.
	// ParseServicesFromProtos leaves such services out; ParseDeclaredServices
	// keeps them, for the consumers that project the wire contract itself.
	Unmanaged bool `json:",omitempty"`
}

ServiceDef represents a parsed Connect RPC service definition.

func ManagedServices added in v0.1.45

func ManagedServices(defs []ServiceDef) []ServiceDef

ManagedServices returns the services in defs that forge manages — those without a `// forge:unmanaged` marker — in their original order.

func ParseDeclaredServices added in v0.1.45

func ParseDeclaredServices(projectDir string) ([]ServiceDef, error)

ParseDeclaredServices reads EVERY service the protos declare, managed or not, with ModulePath set from go.mod. Falls back to empty if the descriptor does not exist yet.

Only for consumers that project the API contract rather than forge's handler model — the open-procedure set (auth_required describes the rpc, whoever implements it), "does this project declare services at all", and the unmanaged-service listing in `forge project audit`. Everything else reads ParseServicesFromProtos.

func ParseServicesFromProtos

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

ParseServicesFromProtos reads the services forge MANAGES from the forge descriptor: every declared service except those marked `// forge:unmanaged`. Falls back to empty if the descriptor does not exist yet.

This is the set every service-scoped consumer reads — handler scaffolds, CRUD, mocks, composition and mounts, cmd subcommands, frontend hooks and pages, audit, doctor, scaffold. Filtering HERE rather than at each consumer is the point: a consumer that forgot to filter would scaffold stubs for handlers that live elsewhere, which is the failure the marker exists to prevent. A consumer that genuinely needs the whole wire contract asks for it by name with ParseDeclaredServices.

func UnmanagedServices added in v0.1.45

func UnmanagedServices(defs []ServiceDef) []ServiceDef

UnmanagedServices returns the services in defs marked `// forge:unmanaged`, in their original order.

func (ServiceDef) GoName added in v0.1.45

func (s ServiceDef) GoName() string

GoName is the base protoc-gen-connect-go builds this service's identifiers from (naming.ConnectServiceGoName): the handler interface is <GoName>Handler, the client <GoName>Client. Name is the proto spelling — which is also what the generated `<Name>Name` constant is spelled from (naming.ConnectServiceNameConst).

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). A narrow consumer-declared interface (or an alias) satisfied by another component is NOT this resolver's job: GenerateCompose layers the go/types assignability pass (producer_assignability.go) over it, which resolves a unique satisfier and refuses loudly when several components satisfy it.

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               // connect-go's base for the service: "EchoService" (Unimplemented<base>Handler)
	ProtoFileSymbol     string               // e.g. "File_services_echo_v1_echo_proto"
	Methods             []MethodTemplateData // method data for handlers.go.tmpl and test templates
	// UnfilledDeps describes the required Deps fields the test harness
	// cannot construct ("Deps.SQL (*sql.DB)", UnfilledDepsFor). When any
	// exist, the scaffolded tests pass WithPlaceholderDeps() so they pass
	// as written.
	UnfilledDeps []string
	// 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
	// HasConfig is whether the project has typed config
	// (ProjectHasTypedConfig): service.go imports <module>/pkg/config and
	// declares Deps.Config only then.
	HasConfig bool
}

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

type TierSchemas added in v0.1.45

type TierSchemas struct {
	Schemas map[string]apiext.JSONSchemaProps // Go type name -> schema
	Docs    map[string]string                 // Go type name -> first doc paragraph
	// Pointers records, per Go type, the JSON fields declared as a Go
	// POINTER. A pointer says that ABSENCE is meaningful (Workload.probes:
	// no probes), which the JSONSchema cannot say, since a pointer and a
	// value lower to the same object schema. The generator reads it so it
	// never defaults such a field to an empty instance.
	Pointers map[string]map[string]bool
}

TierSchemas is the lowered input for GenerateTierKCL: the schema of every type reachable from TierRoots, and each type's Go doc summary.

func LoadTierSchemas added in v0.1.45

func LoadTierSchemas(forgeRoot string) (*TierSchemas, error)

LoadTierSchemas runs controller-tools' parser over the tier package in forgeRoot and returns every reachable type's schema.

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 UnmappedField added in v0.1.45

type UnmappedField struct {
	Message  string    // wire message name ("Order")
	Field    string    // proto field name ("line_items")
	Kind     FieldKind // wire kind ("repeated_message")
	Table    string    // table name ("orders")
	Column   string    // column name ("line_items")
	DeclType string    // declared SQL type ("JSONB")
	Reason   string    // why the pairing has no conversion
}

UnmappedField names one (wire field, column) pair the conversion generator cannot map in a direction that round-trips.

Such a pair is DEAD over the API: the column exists, the wire field exists, and nothing carries a value between them — the field is created as the column DEFAULT forever and never read back. The generator used to emit an explanatory COMMENT in the generated body for exactly this case, which is a guard that cannot fire: the build stays green, the tests stay green, and the only way to learn about it is to read generated code nobody reads. (Measured: a `repeated OrderLineItem line_items` over a JSONB column — Create accepted line items and stored `[]`, List returned none, forever.) Every one of these now fails the generate instead; see UnmappedFieldsError.

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
	// Reason says why, when it is not the default (the package did not
	// load, or the type is not an interface).
	Reason 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 UnsatisfiableColumn added in v0.1.45

type UnsatisfiableColumn struct {
	Entity string // "Customer"
	Table  string // "customers"
	Column string // "company_id"
}

UnsatisfiableColumn is one column forge can prove no generated Create can ever populate: NOT NULL, no DB DEFAULT, excluded from the wire by `forge:read-only`, and declaring no `forge:fill=` strategy.

func FindUnsatisfiableColumns added in v0.1.45

func FindUnsatisfiableColumns(entity EntityDef) []UnsatisfiableColumn

FindUnsatisfiableColumns reports every column of entity that is provably impossible to satisfy through the generated Create path.

A column with no paired wire field at all (plain schema forge never put on the API) is out of scope: this check is specifically about the gap `forge:read-only` leaves, not about every column a Create request happens not to carry — an ordinary internal/denormalized column absent from the API is unaffected by any of this and is not a bug.

type UnversionableEntity added in v0.1.45

type UnversionableEntity struct {
	Entity string // "WorkOrder"
	Table  string // "work_orders"
	Column string // "version"
}

UnversionableEntity is one entity whose `forge:version` column has no paired field on the wire message, so no caller can present the version the concurrency check requires.

func FindUnversionableEntities added in v0.1.45

func FindUnversionableEntities(entity EntityDef) []UnversionableEntity

FindUnversionableEntities reports the entity's version column when the wire message carries no field of the same name.

Returns at most one finding: a second `forge:version` column on one table is a different defect (the repo binds the first it finds) and is not this check's business.

type WireOutcome added in v0.1.45

type WireOutcome int

WireOutcome reports what WireComponentIntoMain did, so the caller can print the right thing.

const (
	// WireUnrecognized means forge did not find the structure it appends to
	// and changed NOTHING. The caller must print the manual instruction.
	WireUnrecognized WireOutcome = iota
	// WireApplied means the constructor (and any missing import) was added.
	WireApplied
	// WireAlreadyWired means the constructor was already referenced; the
	// file was not touched. This is the --resume/--force re-run case.
	WireAlreadyWired
)

func WireComponentIntoMain added in v0.1.45

func WireComponentIntoMain(root, bin, modulePath, group, ctor string) (WireOutcome, error)

WireComponentIntoMain appends `<group>.<ctor>` to the cmd.Execute(...) call in cmd/<bin>/main.go, adding the group subpackage import when it is absent.

It NEVER rewrites a file it cannot confidently parse and locate the call in: every failure to recognize returns WireUnrecognized with the file untouched, so the caller can print the manual instruction instead. The error return is reserved for a real I/O failure on a file forge DID recognize (e.g. the write itself failing) — an unrecognized or absent main.go is an outcome, not an error, because it is a normal state for a project whose owner rewrote it.

func (WireOutcome) String added in v0.1.45

func (o WireOutcome) String() string

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

func DiscoverWorkerSpecs added in v0.1.45

func DiscoverWorkerSpecs(projectDir string) []WorkerSpec

DiscoverWorkerSpecs returns one WorkerSpec per worker package under internal/workers/, sorted by name. Best-effort: a project with no internal/workers/ directory yields nil rather than an error.

Source Files

Jump to

Keyboard shortcuts

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