Documentation
¶
Overview ¶
Package codegen turns Go contract structs into downstream artifacts — the spine of Dockyard's contract-first property (P1, RFC §6.1).
A tool's input and output are typed Go structs; they are the single source of truth. JSON Schema and TypeScript types are generated from them, never hand-authored (AGENTS.md §6). The package implements the Design A pipeline (RFC §6.2, brief 06 §3.1):
┌─ codegen.SchemaFor ─► JSON Schema (Phase 04)
contract struct ───────┤
└─ codegen.TypeScriptFor* ─► TypeScript (Phase 05)
Both generators read Go directly; there is no Node dependency and the two halves never share an intermediate format, so a bug in one cannot silently desync the other.
JSON Schema (Phase 04) ¶
SchemaFor infers a schema; Marshal serializes it deterministically (sorted keys) so regeneration is byte-stable. The schema engine is github.com/google/jsonschema-go — deliberately the same engine the official MCP SDK uses internally (brief 06 §2.3). Picking any other library would create a divergent schema dialect; Dockyard standardizes on this one.
The inference engine infers a property's schema from its Go type alone, so a few real contract shapes need Dockyard-side correction (depth-remediation, D-050/D-051):
- time.Time keeps its format: date-time qualifier (the engine drops it).
- json.RawMessage renders as an unconstrained schema accepting any JSON (the engine renders it as a byte array — an outright wrong schema).
- A named-constant enum (type Severity string + a const set) carries its enum array when the values are registered with WithEnum; EnumsFromSource discovers them from contract source, since reflection cannot see a const block.
- An embedded (anonymous) struct's fields are inlined by the schema (the engine already does this) and by the TypeScript generator (Dockyard flattens them — tygo would otherwise emit a named nested property), matching Go's own encoding/json field promotion.
Recursive contracts ¶
Recursive (self-referential) contracts are supported. SchemaForType detects direct and transitive cycles and emits a deterministic JSON Schema 2020-12 graph using local $defs/$ref references. Generation bounds type-graph depth and node count, and validation accepts only bounded local references; it never dereferences an external $ref or $dynamicRef.
ErrRecursiveContract remains exported for source compatibility with callers compiled against the former recursion limitation. Recursive contracts no longer return it; failures such as exceeding the generation bounds return an error wrapping ErrInvalidContract.
TypeScript (Phase 05) ¶
TypeScriptForSource and TypeScriptForDir convert Go contract source into deterministic TypeScript via github.com/gzuidhof/tygo, an AST-based pure-Go generator that preserves doc comments, enums and constants (brief 06 §2.4). The output carries a "Code generated ... DO NOT EDIT." header and is pinned by golden tests.
Drift cross-check (Phase 05) ¶
Because schema and TypeScript are generated independently, CrossCheck cross-verifies that the two artifacts for one contract describe the same property set, with consistent optionality and consistent value types (a coarse string/number/boolean/array/object kind — D-051), and CheckStale verifies that generated output on disk still matches a fresh regeneration of its Go source. Both hard-fail (RFC §6.2, brief 06 R1) — they are the seam Phase 18's `dockyard validate` command calls. Stale generated output is a build blocker, never a warning.
Index ¶
- Constants
- Variables
- func CheckStale(onDisk, fresh []byte) error
- func CrossCheck(schema *jsonschema.Schema, tsTypeName string, ts []byte) error
- func EnumValuesFromSource(goSource string) (map[string][]any, error)
- func Marshal(s *jsonschema.Schema) ([]byte, error)
- func OutputSchemaFor[T any](opts ...SchemaOption) (*jsonschema.Schema, error)
- func RegisterEnumMetadata(typeName string, values ...any)
- func SchemaFor[T any](opts ...SchemaOption) (*jsonschema.Schema, error)
- func SchemaForType(t reflect.Type, opts ...SchemaOption) (*jsonschema.Schema, error)
- func TypeScriptForDir(dir string, opts ...TSOption) ([]byte, error)
- func TypeScriptForSource(goSource string, opts ...TSOption) ([]byte, error)
- func ValidateSchema(raw []byte, requireObject bool) (*jsonschema.Schema, error)
- type SchemaOption
- type TSOption
Constants ¶
const Draft202012 = "https://json-schema.org/draft/2020-12/schema"
Draft202012 is the only JSON Schema dialect emitted and accepted by Dockyard.
const GeneratedSchemaComment = "Code generated by dockyard; DO NOT EDIT."
GeneratedSchemaComment marks schemas that Dockyard may safely manage as generated artifacts. Ownership-index hashes are necessary but not sufficient evidence for destructive orphan cleanup.
Variables ¶
var ErrInvalidContract = errors.New("dockyard/internal/codegen: invalid contract type")
ErrInvalidContract is returned when a Go type cannot be expressed as a tool-contract JSON Schema. It wraps the underlying inference failure so callers can branch with errors.Is.
var ErrNonconformantSchema = errors.New("dockyard/internal/codegen: nonconformant JSON Schema")
ErrNonconformantSchema identifies schemas rejected by Dockyard's policy.
var ErrRecursiveContract = fmt.Errorf("%w: recursive (self-referential) contract type", ErrInvalidContract)
ErrRecursiveContract is retained for callers compiled against the former recursion limitation. Recursive contracts now generate local $defs/$ref graphs and no new operation returns this error.
var ErrSchemaTSDrift = errors.New("dockyard/internal/codegen: schema/typescript drift")
ErrSchemaTSDrift is returned when the generated JSON Schema and the generated TypeScript for the same contract describe different property sets or inconsistent optionality. Design A generates the two artifacts independently from Go (RFC §6.2), so a bug in either generator could silently desync them; CrossCheck makes that desync a hard failure. Callers branch with errors.Is.
var ErrStaleGenerated = errors.New("dockyard/internal/codegen: stale generated output")
ErrStaleGenerated is returned when a generated artifact on disk no longer matches a fresh regeneration from its Go source. Stale generated output is a build blocker, never a warning (RFC §6.2, brief 06 R1). Callers branch with errors.Is.
var ErrTypeScriptGen = errors.New("dockyard/internal/codegen: typescript generation failed")
ErrTypeScriptGen is returned when Go contract source cannot be converted to TypeScript. It wraps the underlying tygo parse/convert failure so callers can branch with errors.Is.
Functions ¶
func CheckStale ¶
CheckStale reports whether on-disk generated output is stale versus a fresh regeneration. onDisk is the committed generated file (a schema JSON or a contracts.ts); fresh is a freshly generated artifact for the same contract source. It returns an error wrapping ErrStaleGenerated when the two differ — which means the Go source changed without `dockyard generate` being rerun.
The comparison is byte-exact: both Marshal (schema) and TypeScriptForSource (TS) are deterministic, so any difference is a real change in the contract source, never formatting churn.
func CrossCheck ¶
func CrossCheck(schema *jsonschema.Schema, tsTypeName string, ts []byte) error
CrossCheck verifies the generated JSON Schema and the generated TypeScript for one contract agree. It is the drift cross-check at the heart of RFC §6.2 and the seam Phase 18's `dockyard validate` calls.
schema is the contract's JSON Schema (from SchemaFor). tsTypeName is the name of the TypeScript interface for the same contract — e.g. "ShowRevenueOutput". ts is the generated TypeScript (from TypeScriptForSource), normally the whole contracts.ts file; CrossCheck locates the named interface within it.
It fails — returning an error wrapping ErrSchemaTSDrift — when:
- the schema is not an object schema (a tool contract must be an object);
- the TypeScript interface tsTypeName is absent;
- a property is present in one artifact and absent in the other;
- a property's optionality disagrees (required in the schema but optional in TypeScript, or the reverse);
- a property's value-type disagrees — a string in one artifact and a number/array/object in the other (D-051).
CrossCheck compares the property name set, optionality, and a coarse value-type kind (string / number / boolean / array / object). It does not walk the full type graph — it does not descend into nested objects or array element types — but a same-named property whose top-level kind diverges between the two artifacts is caught as drift. Named generated interfaces and aliases are resolved to their declaration kind; `any`/`unknown` cannot mask a constrained schema. An unconstrained schema remains compatible with every TS kind. Base64 schema strings must be explicit TypeScript strings.
It expects the default optional style (`field?: T`); generate the TypeScript without WithNullOptional for the artifact passed here. WithNullOptional renders an optional field as `field: T | null` with no `?` marker, which parseTSInterface reads as required — see the documented limitation pinned by TestCrossCheck_WithNullOptionalIsMisclassified.
func EnumValuesFromSource ¶ added in v1.9.0
EnumValuesFromSource returns the source-visible enum sets used by the real generate path. The returned map and slices are newly allocated.
func Marshal ¶
func Marshal(s *jsonschema.Schema) ([]byte, error)
Marshal serializes a schema to indented JSON deterministically: identical input always yields byte-identical output. Determinism is what makes regeneration safe and golden tests meaningful (brief 06 R1) — a drift in the generated schema, or a regression in the upstream inference engine, surfaces as a visible diff rather than churn.
The jsonschema.Schema marshaller already renders object properties in struct field order via its PropertyOrder field; Marshal re-indents that output with two-space indentation and a trailing newline for a stable on-disk form.
func OutputSchemaFor ¶ added in v1.9.0
func OutputSchemaFor[T any](opts ...SchemaOption) (*jsonschema.Schema, error)
OutputSchemaFor infers an output contract. Unlike tool input, MCP structured output may be any JSON value.
func RegisterEnumMetadata ¶ added in v1.9.0
RegisterEnumMetadata installs generated source metadata for runtime schema inference. Values are copied and keyed by package-qualified Go type name.
func SchemaFor ¶
func SchemaFor[T any](opts ...SchemaOption) (*jsonschema.Schema, error)
SchemaFor infers a JSON Schema for the contract type T (RFC §6.1, P1).
T is normally a tool's input or output struct. A tool contract's top-level type must be an object — a struct or a string-keyed map — because the MCP spec requires tool input/output schemas to have JSON type "object" (RFC §6.3; SDK behaviour, runtime/server.AddTool). SchemaFor enforces that and returns an error wrapping ErrInvalidContract otherwise, so a misdeclared contract fails in Dockyard's own validation rather than at runtime inside a host.
Inference is delegated to github.com/google/jsonschema-go — the same engine the official MCP SDK uses (brief 06 §2.3) — so Dockyard emits exactly one schema dialect. Property names come from `json` tags; `omitempty`/`omitzero` fields are optional, all others required; a `jsonschema` struct tag becomes a property description. time.Time carries format: date-time and json.RawMessage is an unconstrained schema (D-050). Pass WithEnum to attach an `enum` array for a named-constant type (D-051). Recursive contracts use local $defs/$ref.
func SchemaForType ¶
func SchemaForType(t reflect.Type, opts ...SchemaOption) (*jsonschema.Schema, error)
SchemaForType is SchemaFor for a reflect.Type rather than a type parameter. It is the seam Phase 06's manifest loader uses to resolve a Go type reference named in dockyard.app.yaml to its schema without a compile-time type argument.
func TypeScriptForDir ¶
TypeScriptForDir reads the .go files of a contracts directory and generates TypeScript for the type declarations they contain. It is the seam Phase 18's `dockyard generate` calls with a project's `internal/contracts` directory.
Files are read in sorted filename order and their type declarations concatenated in that order, so the generated output is deterministic regardless of filesystem iteration order. Test files (`_test.go`) and generated files are skipped.
func TypeScriptForSource ¶
TypeScriptForSource converts a fragment of Go contract source — the type declarations of a contracts package — into deterministic TypeScript.
goSource is ordinary Go source: a package clause is permitted and ignored, imports are stripped, and only top-level type/const declarations are converted (tygo reads the Go AST, not reflection, so doc comments, enums and constants survive — brief 06 §2.4). The returned bytes carry the Code-generated header and end with a trailing newline; identical input always yields byte-identical output, which is what makes the golden tests and the drift cross-check meaningful (RFC §6.2, brief 06 R1).
On a tygo parse or convert failure the error wraps ErrTypeScriptGen.
Malformed Go source — a struct field carrying a syntactically invalid struct tag is the known case — can drive the tygo dependency to panic rather than return an error (tygo parses tags via reflect.StructTag, which panics on a malformed pair). TypeScriptForSource contains that panic and converts it to an ErrTypeScriptGen error: a malformed contract file must fail the codegen step cleanly, never crash the process (CLAUDE.md §13 — never panic across a process boundary). Found by the Phase 21.5 FuzzTypeScriptForSource target.
func ValidateSchema ¶ added in v1.9.0
func ValidateSchema(raw []byte, requireObject bool) (*jsonschema.Schema, error)
ValidateSchema applies Dockyard's bounded, local-only 2020-12 policy.
Types ¶
type SchemaOption ¶
type SchemaOption func(*schemaConfig)
SchemaOption configures schema generation.
func EnumsFromSource ¶
func EnumsFromSource(goSource string) ([]SchemaOption, error)
EnumsFromSource parses Go contract source and discovers named-type enum constant sets — a `type Severity string` (or integer) declaration paired with a `const` block of typed values — returning a SchemaOption for each (D-051).
It is the seam the `generate` pipeline uses to feed WithEnum automatically: the schema generator works from reflection, which cannot see a `const` block, so the constant *values* must come from the source. EnumsFromSource bridges that — pass it the same contract source handed to TypeScriptForSource, then splat the result into SchemaFor:
enumOpts, err := codegen.EnumsFromSource(src) schema, err := codegen.SchemaForType(t, enumOpts...)
Only constants with an explicit named type and a literal string or integer value are collected; an untyped const, or one with a non-literal initializer, is skipped (it cannot be expressed as a static `enum`). On a parse failure the error wraps ErrInvalidContract.
func WithEnum ¶
func WithEnum(typeName string, values ...any) SchemaOption
WithEnum registers the constant set of a named contract type so the generated schema carries an `enum` array for every property of that type (D-051).
typeName is the bare Go type name — "Severity", not "contracts.Severity". values are the JSON values of that type's constants.
The inference engine — github.com/google/jsonschema-go — infers a property's schema from its Go *type* only: a `type Severity string` field renders as a plain {"type":"string"}, and the named type's `const` set is invisible to reflection, so the `enum` array is lost. That makes the schema diverge from the TypeScript artifact, which tygo *does* emit as a union. WithEnum closes the gap: SchemaFor post-processes the schema to attach `enum` to every matching property (top-level, nested, slice items, and map values).
The `generate` pipeline discovers these from contract source with EnumsFromSource; callers with a static contract may pass them directly:
codegen.SchemaFor[EventRecord](
codegen.WithEnum("Severity", "info", "warn", "error"))
type TSOption ¶
type TSOption func(*tsConfig)
TSOption configures TypeScript generation.
func WithIndent ¶
WithIndent sets the indentation string of the generated TypeScript. The default is two spaces, matching the JSON Schema marshaller (codegen.Marshal) so both generated artifacts share a house style.
func WithNullOptional ¶
func WithNullOptional() TSOption
WithNullOptional renders optional fields as `T | null` rather than the tygo default `field?: T` (`undefined`). Use it when the consuming UI deserializes JSON that carries explicit nulls for absent optional fields.