codegen

package
v1.7.3 Latest Latest
Warning

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

Go to latest
Published: Jun 2, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

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.

Recursion — a documented V1 limitation

A recursive (self-referential) contract — a type that, directly or transitively, contains itself — is not supported in V1 (D-052). JSON Schema expresses cycles with $ref/$defs, but github.com/google/jsonschema-go does not emit $defs for recursive Go types: it hard-fails inside its reflection walk and exposes no hook to break the cycle or post-process it into a $ref. SchemaForType detects the cycle up front and returns ErrRecursiveContract — a specific, actionable error citing this limitation — rather than leaking the engine's vague internal "cycle detected" string. The TypeScript generator (tygo) handles recursion natively, so only the schema half is limited.

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

This section is empty.

Variables

View Source
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.

View Source
var ErrRecursiveContract = fmt.Errorf("%w: recursive (self-referential) contract type", ErrInvalidContract)

ErrRecursiveContract is returned when a contract type is recursive or self-referential (a type that, directly or transitively, contains itself).

Recursion is an explicit, documented V1 limitation of the schema generator (see D-052). JSON Schema can express cycles with `$ref`/`$defs`, but the pinned inference engine — github.com/google/jsonschema-go, the single schema dialect Dockyard standardizes on (RFC §6.2) — does not emit `$defs` for recursive Go types: it hard-fails with an internal "cycle detected" deep in its reflection walk, and exposes no hook to break the cycle or post-process it into a `$ref`. Rather than leak that vague upstream string, SchemaForType detects the cycle up front and returns this specific, actionable error.

ErrRecursiveContract also wraps ErrInvalidContract, so existing callers that branch on errors.Is(err, ErrInvalidContract) keep working.

View Source
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.

View Source
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.

View Source
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

func CheckStale(onDisk, fresh []byte) error

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, which is exactly the failure mode findings 1–4 of the depth audit produced. A property typed by a named type (an enum or a nested interface) or by an unconstrained schema is treated as kind-compatible and skipped for the type comparison, so a legitimately opaque field never reports false drift.

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 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 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). A recursive contract returns ErrRecursiveContract (D-052).

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

func TypeScriptForDir(dir string, opts ...TSOption) ([]byte, error)

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

func TypeScriptForSource(goSource string, opts ...TSOption) ([]byte, error)

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.

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

func WithIndent(indent string) TSOption

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.

Jump to

Keyboard shortcuts

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