Go builder
About 4563 wordsAbout 15 min
2026-10-01
Every exported symbol of package build, import path github.com/spechtlabs/sigil/pkg/build, and the Sigil each one renders. The package writes modules and policies in Go against a policy.Kind and renders them to .sigil source.
To build, commit and check a module step by step, see Build policies in Go. The API is pre-1.0, so names and signatures can still change.
Why: Facts, vocabulary and rules.
Documents
Module and Policy
func Module[In any](name string, k *policy.Kind[In], body func(m *ModuleDoc[In], in *In), opts ...DocOption) *ModuleDoc[In]
func Policy[In any](name string, k *policy.Kind[In], body func(p *PolicyDoc[In], in *In), opts ...DocOption) *PolicyDoc[In]| Parameter | Is |
|---|---|
In | The kind's input struct |
name | The document's name, as its header declares it: deploy.freeze |
k | The kind the document is written against |
body | Builds the document's statements. Runs once, inside the call. in is the shadow input |
opts | Document options |
- The header pins the kind's current version,
module deploy.freeze: DeployApproval@2, unlessWithPinpins an older one. inis a zero value ofInwhose pointer-to-struct fields are allocated, recursively and cycle-safe, so&in.Release.Parent.Authoris a valid address. It's only for taking addresses; its values are never read.- A module holds lets and the imports they need. A policy holds params, lets, rules and invocations.
- Nothing panics. Every mistake is collected with its Go call site and returned by
Sourceand the output functions asErrors. - A nil
korbodydoesn't panic either: a nil kind is an error, and a nil body builds an empty document.
freeze := build.Module("deploy.freeze", kind, func(m *build.ModuleDoc[Input], in *Input) {
m.Comment("Frozen when the freeze names the environment, or when nobody knows.")
build.Pub(m, "is_frozen", build.Or(
build.Field(&in.Freeze.Unknown),
build.In(build.Field(&in.Environment), build.Field(&in.Freeze.Environments)),
))
})// Code generated by pkg/build from example_test.go. DO NOT EDIT.
module deploy.freeze: DeployApproval@1
// Frozen when the freeze names the environment, or when nobody knows.
pub let is_frozen = freeze.unknown or environment in freeze.environmentsDocument types
type ModuleDoc[In any] struct{ /* unexported fields */ }
type PolicyDoc[In any] struct{ /* unexported fields */ }
type Doc interface {
Importable
Path() string
Source() ([]byte, error)
// unexported method
}
type Importable interface {
Name() string
// unexported method
}
type Invocable interface {
Importable
// unexported method
}
type ExternDoc struct{ /* unexported fields */ }
func Extern(name string) *ExternDoc
func (e *ExternDoc) Name() string
type Alias struct{ /* unexported fields */ }
func As(target Importable, alias string) *Alias
func (a *Alias) Name() string| Type | Implemented by | Used by |
|---|---|---|
Doc | *ModuleDoc, *PolicyDoc | The output functions |
Importable | *ModuleDoc, *PolicyDoc, *ExternDoc, *Alias | Ref |
Invocable | *PolicyDoc, *ExternDoc, *Alias | Invoke |
| Method | Returns |
|---|---|
Name() | The document's name, the one in its header; for an *Alias, the aliased document's |
Path() | Where the rendered file goes, relative to a policy directory: the name with dots as slashes, deploy/freeze.sigil, unless WithPath sets it |
Source() | The rendered source, or Errors |
Extern(name)names a module or policy written by hand, so a document built in Go canRefits pub lets orInvokeit.- An invalid
Externname is an error of the document that uses it. As(target, alias)importstargetunderalias; see Imports.
Document options
type DocOption interface{ /* unexported method */ }
func WithPath(path string) DocOption
func WithPin(n int) DocOption
func WithHeader(text string) DocOption| Option | Does | Default |
|---|---|---|
WithPath(path) | Sets the rendered file's path. A valid io/fs path ending in .sigil, with no element starting with ., since Load skips those | The name with dots as slashes |
WithPin(n) | Pins kind version n in the header, for a document that must still load in hosts on an older version while a kind change rolls out. From the oldest version the kind accepts to the current one | The kind's current version |
WithHeader(text) | Sets the comment the file starts with, one // line per line of text, split at \r\n, \r and \n. "" leaves it out | Code generated by pkg/build from <file>.go. DO NOT EDIT., naming the Go file of the Module or Policy call |
build.WithPath("platform/deploy/gate.sigil"),
build.WithHeader("Generated from internal/deploy/vocabulary.go.\nRun go generate ./internal/deploy to update."),// Generated from internal/deploy/vocabulary.go.
// Run go generate ./internal/deploy to update.
policy deploy.gate: DeployApproval@2Declarations
func Let[T any](s Scope, name string, x Expr[T]) Expr[T]
func Pub[T any](s TopScope, name string, x Expr[T]) Expr[T]
func Param[T any](p ParamScope, name string, opts ...ParamOption[T]) Expr[T]
func Default[T any](v T) ParamOption[T]
func Min[T any](v T) ParamOption[T]
func Max[T any](v T) ParamOption[T]| Call | Renders | Scope |
|---|---|---|
build.Let(s, "short", x) | let short = x | *ModuleDoc, *PolicyDoc or *Block (Scope) |
build.Pub(s, "is_frozen", x) | pub let is_frozen = x | *ModuleDoc or *PolicyDoc (TopScope) |
build.Param[[]string](p, "approvers") | param approvers: list<string> | *PolicyDoc (ParamScope) |
build.Param(p, "min_soak", build.Default(24*time.Hour), build.Min(time.Hour), build.Max(48*time.Hour)) | param min_soak: duration = 24h, min: 1h, max: 48h | *PolicyDoc |
- Each returns an
Exprthat reads the let or param. - A
Letin a*Blockis scoped to thatwhenbody and the bodies nested in it. Reading it elsewhere is an error. - A param's Sigil type is the one
Tmaps to under the kind's Go type mapping. An optionalTcan't be a param. Default,MinandMaxrender as literals; each may be given once. The bounds' rules are in Bounds.- Every let and param name is declared once per document. A second declaration is an error naming the first.
- A let or param read from another document is an error; read another document's pub let with
Ref. - A nil scope is an error of the document that reads the let.
Statements
type Block struct{ /* unexported fields */ }
type Argument struct{ /* unexported fields */ }
func (p *PolicyDoc[In]) When(cond Expr[bool], body func(b *Block))
func (p *PolicyDoc[In]) Assert(reason string, cond Expr[bool])
func (p *PolicyDoc[In]) Decide(o policy.Outcome, args ...Argument)
func (p *PolicyDoc[In]) Invoke(target Invocable, args ...Argument)
func (p *PolicyDoc[In]) Comment(text string)
func (m *ModuleDoc[In]) Comment(text string)
func Arg[T any](name string, x Expr[T]) Argument*Block has the same When, Assert, Decide, Invoke and Comment methods.
| Call | Renders |
|---|---|
p.When(cond, func(b *build.Block) { … }) | when cond { … } |
b.Assert("needs_team_label", cond) | assert("needs_team_label", cond) |
b.Decide(deploy.Deny.Reason("change_freeze")) | deny(reason: change_freeze) |
b.Decide(deploy.Review.Reason("service_owner"), build.Arg("approvers", approvers)) | review(reason: service_owner, approvers: approvers) |
p.Invoke(build.Extern("deploy.guardrails"), build.Arg("min_soak", minSoak)) | guardrails(min_soak: min_soak), and use deploy.guardrails |
p.Comment("Owners review their own services.") | // Owners review their own services. before the next statement |
Decidetakes the decision and reason from a reason handle. A misspelled reason panics at the handle, not here. Areasonargument is an error.Argnames a payload field or an invoked policy's param. A name given twice is an error.- Invocation arguments are constants and the invoking policy's own params, as in Policy invocation.
- A
Commenttext of several lines, split at\r\n,\rand\n, renders as several comment lines. On a module with no statement after it, the comment ends the file. - A zero
policy.Outcomeand a nilInvoketarget are errors.
A policy built from these:
gate := build.Policy("deploy.gate", kind, func(p *build.PolicyDoc[Input], in *Input) {
approvers := build.Param[[]string](p, "approvers")
minSoak := build.Param(p, "min_soak", build.Default(24*time.Hour), build.Min(time.Hour), build.Max(48*time.Hour))
tiers := build.Param(p, "tiers", build.Default([]Tier{Standard}))
p.Invoke(guardrails, build.Arg("min_soak", minSoak))
p.Comment("Owners review their own services.")
p.When(build.Ref[bool](common, "owns_service"), func(b *build.Block) {
short := build.Let(b, "short", build.Field(&in.Release.Soak).Lt(build.Lit(4*time.Hour)))
b.Assert("needs_team_label", build.HasKey(build.Field(&in.Service.Labels), build.Lit("team")))
b.When(build.And(build.In(build.Field(&in.Service.Tier), tiers), build.Not(short)), func(b *build.Block) {
b.Decide(review.Reason("service_owner"), build.Arg("approvers", approvers))
})
b.When(build.Ref[bool](guardrails, "is_hotfix"), func(b *build.Block) {
b.Decide(deny.Reason("soak_too_short"))
})
})
})// Code generated by pkg/build from gate.go. DO NOT EDIT.
policy deploy.gate: DeployApproval@2
use deploy.common.{owns_service}
use deploy.guardrails
param approvers: list<string>
param min_soak: duration = 24h, min: 1h, max: 48h
param tiers: list<Tier> = [standard]
guardrails(min_soak: min_soak)
// Owners review their own services.
when owns_service {
let short = release.soak < 4h
assert("needs_team_label", service.labels has "team")
when service.tier in tiers and not short {
review(reason: service_owner, approvers: approvers)
}
when guardrails.is_hotfix {
deny(reason: soak_too_short)
}
}Expressions
type Expr[T any] struct{ /* unexported fields */ }
type Value interface{ /* unexported method */ }Tis the Go type a host value of the expression has:string,bool,intorint64,float64,time.Duration,time.Time,[]E,map[K]V, an enum type the kind registers,*Tfor an optional, or a struct type of the kind.Tonly guides the Go compiler.Lton two strings builds; the Sigil checker, whichCheckruns, decides what's well typed.- Every
Expris aValue, the typeCalltakes. Nothing else implementsValue. - The zero
Exprholds no expression. Using one is an error at the call that received it.
Inputs and literals
func Field[T any](p *T) Expr[T]
func Opt[T any](p *T) Expr[*T]
func OptPtr[T any](p **T) Expr[*T]
func Lit[T any](v T) Expr[T]
func List[E any](xs ...Expr[E]) Expr[[]E]
func Raw[T any](src string) Expr[T]| Call | Renders |
|---|---|
build.Field(&in.Release.Soak) | release.soak |
build.Field(&in.Release.Parent), a *Parent field | release.parent, an Expr[*Parent] |
build.Opt(&in.Release.Parent.Author) | release.parent?.author |
build.OptPtr(&in.Release.Parent.MergedBy), a *string field | release.parent?.merged_by |
build.Lit(24 * time.Hour) | 24h |
build.List(build.Field(&in.Environment), build.Lit("canary")) | [environment, "canary"] |
build.Raw[bool](`environment == "prod" or environment == "staging"`) | environment == "prod" or environment == "staging", in parentheses where an operator needs them |
Fieldtakes the address of a tagged field of the body'sin, of a binder's variable, or of a field of either. The path comes from thepolicytags when the document renders, matched by address and Go type.Fieldon a path through an optional struct is an error namingOpt.OptandOptPtron a path through none is an error namingField.Opton a pointer field reached through an optional struct is an error namingOptPtr.OptPtrisOptfor a pointer field reached through an optional struct. Optionals don't nest, so the result is optional once.- A field without a
policytag, the input as a whole, and a pointer that's nil or points elsewhere are errors. - A list element or a map value has no address in the shadow, whose slices and maps are empty:
&in.Release.Commits[0].Authorpanics with an index out of range. Read it withIndexorGet, thenSel. - Two tagged fields that take no memory, such as two fields of an empty struct type, share an address and a type. A pointer to one of them is an error.
Rawmust parse as one expression. It's reprinted from its syntax tree, so a comment in it is dropped. It names inputs and lets by hand, so a renamed Go field or tag doesn't reach it.
Literals
| Go value | Renders |
|---|---|
"prod\"x" | "prod\"x", quoted as Go quotes it |
true | true |
3, int64(3) | 3 |
0.5 | 0.5 |
36*time.Hour + 30*time.Minute | 36h30m |
Critical, a value of an enum type | critical |
| A value two enums of the kind declare | Tier.critical |
[]string{"a", "b"} | ["a", "b"] |
map[string]string{"z": "1", "a": "2"} | {"a": "2", "z": "1"}, sorted by key |
An empty slice or map renders [] or {}, as does List() with no expressions. It takes its type from where it stands, such as the other operand of in or a param's type. Standing alone, as in pub let none = [], it fails Check:
deploy/empty.sigil:3:16: error: cannot infer the type of `[]`
|
3 | pub let none = []
| ^^
= help: add an element, or use the literal where a typed list or map is expected
= go: vocabulary.go:71: build.PubNo literal, an error when the document renders: a time.Time, an optional, a struct, a duration with a remainder below a millisecond, and a float that isn't finite.
vocabulary.go:75: build.Lit: timestamp has no literal: a timestamp comes from input onlyOperators
Methods of Expr[T]:
| Method | Renders |
|---|---|
x.Eq(y), x.NotEq(y) | x == y, x != y |
x.Lt(y), x.Le(y), x.Gt(y), x.Ge(y) | x < y, x <= y, x > y, x >= y |
x.Within(s) | x in s, substring |
x.Like("prod-*") | x like "prod-*" |
x.Matches(`^prod-\d+$`) | x matches `^prod-\d+$` |
x.Add(y), x.Sub(y) | x + y, x - y, on numbers and durations |
- The comparisons and
WithinreturnExpr[bool];AddandSubreturnExpr[T]. Matchesrenders a pattern with a backslash as a raw string, unless a raw string can't hold it: a pattern with a backtick, a control character such as a line ending, or invalid UTF-8 goes into a quoted string. An invalid RE2 pattern is an error when the document renders.
Functions, where a method can't express the types:
| Function | Renders |
|---|---|
build.And(a, b, …), build.Or(a, b, …) | a and b and …, a or b or …; one operand renders as itself, none is an error |
build.Xor(a, b) | a xor b |
build.Not(x) | not x |
build.In(x, xs), build.NotIn(x, xs) | x in xs, x not in xs |
build.AllIn(a, b), build.AnyIn(a, b) | a all in b, a any in b |
build.OneIn(a, b), build.ExclusiveIn(a, b) | a one in b, a exclusive in b |
build.Index(xs, build.Lit(0)) | xs[0]; the index is an int or int64 expression |
build.Get(m, k) | m[k] |
build.HasKey(m, k) | m has k |
build.HasAll(m, sub) | m has sub |
build.Coalesce(x, def) | x ?? def |
build.Present(x) | present x |
build.Neg(x) | -x |
build.TimeAdd(t, d), build.TimeSub(t, d) | t + d, t - d, a time.Time |
build.TimeDiff(a, b) | a - b, a time.Duration |
build.Sel(x, func(c *Commit) *string { return &c.Author }) | x.author |
Selreads a field of a struct value that has no address to take, such asbuild.Index(commits, build.Lit(0)), which rendersrelease.commits[0].author. The path may not cross an optional struct.- Each operator's operand types and semantics are in Expressions.
Binders
func Any[E any](name string, xs Expr[[]E], body func(e *E) Expr[bool]) Expr[bool]
func All[E any](name string, xs Expr[[]E], body func(e *E) Expr[bool]) Expr[bool]
func Filter[E any](name string, xs Expr[[]E], body func(e *E) Expr[bool]) Expr[[]E]| Call | Renders |
|---|---|
build.Any("r", roles, func(r *string) build.Expr[bool] { return build.Field(r).Like("sre-*") }) | any r in actor.roles: r like "sre-*" |
build.All("c", commits, func(c *Commit) build.Expr[bool] { return build.In(build.Field(&c.Author), teams) }) | all c in release.commits: c.author in actor.teams |
build.Filter("r", roles, func(r *string) build.Expr[bool] { return build.Field(r).Like("sre-*") }) | filter r in actor.roles: r like "sre-*" |
bodygets a fresh*E, allocated like the input shadow.build.Field(e)is the variable,build.Field(&e.Name)a field of it.- The variable is valid only inside
body. A variable name that isn't an identifier, or a nilbody, is an error.
Host functions
func Call[R any](name string, args ...Value) Expr[R]
func Func1[A, R any](name string) func(Expr[A]) Expr[R]
func Func2[A, B, R any](name string) func(Expr[A], Expr[B]) Expr[R]
func Func3[A, B, C, R any](name string) func(Expr[A], Expr[B], Expr[C]) Expr[R]| Call | Renders |
|---|---|
build.Call[[]string]("split", labels, build.Lit(",")) | split(service.labels["regions"], ",") |
lower := build.Func1[string, string]("lower"), then lower(build.Field(&in.Service.Name)) | lower(service.name) |
nameis the name the kind'sWithFuncgives the function.Ris its Go result type.Func1,Func2andFunc3type the arguments too. Declare one once and call it like the function.
Parentheses and layout
The printer follows the precedence table and adds only the parentheses the parser needs to build the same tree:
| Built | Renders |
|---|---|
build.Not(build.Or(a, b)) | not (environment == "a" or release.hotfix) |
a.Eq(b) where a is a comparison | (environment == "a") == release.hotfix |
build.Or(build.Xor(a, b), c) | (environment == "a" xor release.hotfix) or false |
build.And(build.Any(…), b) | (any r in actor.roles: r like "sre-*") and release.hotfix |
build.Neg(build.Neg(soak)) | - -release.soak |
- Comparisons,
in,has,likeandmatchesare non-associative, so one as an operand of another gets parentheses. xordoesn't chain or mix withorwithout parentheses.- A quantifier or filter body extends as far right as it can, so a binder gets parentheses unless it's the rightmost operand. A binder body whose top level is
and,ororxorgets them too. ??is right-associative.- An
and,ororxorchain of three operands or more, or one that runs past 88 columns, breaks before each operator. - A let's value goes on the next line when it breaks or doesn't fit after the
=. - A list or map literal that doesn't fit on its line puts one item per line, with a trailing comma.
- The result is formatted by
sigil fmt's printer, so it passessigil fmt --checkas it is.
pub let chain =
environment == "production"
and release.hotfix
and "deployer" in actor.rolesImports
func Ref[T any](d Importable, name string) Expr[T]| Reads | Import | Renders |
|---|---|---|
build.Ref[bool](freeze, "is_frozen") | use deploy.freeze.{is_frozen} | is_frozen |
build.Ref[bool](guardrails, "is_hotfix") in a document that also invokes guardrails | use deploy.guardrails | guardrails.is_hotfix |
- A document's imports come from its
RefandInvokecalls. Nothing is imported by hand. - One
useper path, sorted by path. A selective import lists its names sorted. - A path the document invokes is imported whole, and every
Refto it is qualified by its last segment. An aliased path is imported whole too, and qualified by its alias. dis a*ModuleDoc, a*PolicyDoc, anExternor anAsalias. For a document built in Go, a name it doesn't export as apub letis an error listing what it exports. AnExtern's names are checked byCheck.- Two imports that bind the same name, or an import that takes the name of one of the document's lets or params, are errors. Nothing is renamed unless
Asgives the alias. - A document can't import itself, or a document built for another kind.
vocabulary.go:73: build.Ref: deploy.common has no pub let owner; it exports: owns_service
vocabulary.go:74: build.Ref: importing owns_service from team.common collides with owns_service imported from deploy.common; every name in a document means one thingAs
func As(target Importable, alias string) *Alias- Imports
targetunderalias, for a document that reads or invokes two documents whose names end alike, such asplatform.guardrailsandpayments.guardrails. - An aliased document is always imported whole,
use payments.guardrails as payments_guardrails. An invocation callspayments_guardrails(…), and aRefreadspayments_guardrails.is_hotfix. - A document imports another under one name. Reading it both with and without the alias, or under two aliases, is an error.
platform := build.Extern("platform.guardrails")
payments := build.As(build.Extern("payments.guardrails"), "payments_guardrails")
gate := build.Policy("payments.gate", kind, func(p *build.PolicyDoc[Input], in *Input) {
p.Invoke(platform)
p.Invoke(payments, build.Arg("min_soak", build.Lit(4*time.Hour)))
p.When(build.Ref[bool](payments, "is_hotfix"), func(b *build.Block) {
b.Decide(deny.Reason("soak_too_short"))
})
}, build.WithPin(2), build.WithHeader(""))policy payments.gate: DeployApproval@2
use payments.guardrails as payments_guardrails
use platform.guardrails
guardrails()
payments_guardrails(min_soak: 4h)
when payments_guardrails.is_hotfix {
deny(reason: soak_too_short)
}Output
func FS(docs ...Doc) (fs.FS, error)
func Write(dir string, docs ...Doc) error
func Diff(fsys fs.FS, docs ...Doc) error
func Check[In any](k *policy.Kind[In], base fs.FS, docs ...Doc) error| Function | Does | Error |
|---|---|---|
FS(docs...) | Renders the documents into an in-memory fs.FS, each at its Path(), for Load, From and Trusted | Errors |
Write(dir, docs...) | Writes each rendered document to its Path() under dir, creating directories. Writes nothing when a document doesn't render | Errors, or the error from writing |
Diff(fsys, docs...) | Compares the files in fsys at each Path() with the rendered bytes. nil when every file matches | *DriftError, or Errors |
Check(k, base, docs...) | Type-checks the rendered documents against k, in one bundle with the .sigil files of base. nil when the bundle checks | *CheckError, or Errors |
- Each function renders every document first and returns
Errorswhen one doesn't render. - Two documents with the same name, the same path, or paths that differ only in case are errors. A file system that ignores case keeps only one of the two files.
Writenever removes a file. A document that was renamed or dropped leaves its old file behind, whichDiffdoesn't see.Check:baseholds the documents written by hand and may be nil. A rendered document replaces a file ofbaseat the same path. Modules are checked like policies, so a module needs no policy that imports it. Lints don't run.Check: a document built for another kind thankis an error.
DriftError
type DriftError struct {
Stale []string // files whose content differs from the rendered source
Missing []string // files that don't exist
// unexported fields
}
func (e *DriftError) Error() stringError() lists every stale and missing file, with a unified diff of each stale one. A file whose only difference is CRLF line endings is stale too, and the error says so instead of diffing every line:
build: 1 rendered file(s) out of date; regenerate them from the Go code
deploy/env.sigil is stale:
--- deploy/env.sigil (on disk)
+++ deploy/env.sigil (rendered)
@@ -3,4 +3,4 @@
pub let development = environment == "development"
pub let staging = environment == "staging"
pub let canary = environment == "canary"
-pub let production = environment == "prod"
+pub let production = environment == "production"build: 1 rendered file(s) out of date; regenerate them from the Go code
deploy/env.sigil is stale:
only the line endings differ: CRLF on disk, LF rendered; a checkout that converts them, like git's core.autocrlf, does that, so add `*.sigil text eol=lf` to .gitattributesCheckError
type CheckError struct {
Diagnostics []Diagnostic
// unexported fields
}
func (e *CheckError) Error() string
type Diagnostic struct {
Site *Site // nil in a document written by hand
Message string // what's wrong, on one line
Help string // how to fix it; empty when there's no obvious fix
Position policy.Position // start of the offending source
End policy.Position // just after it
}Error()renders every diagnostic the way the CLI does, see Error messages, and adds a= go:line to each one in a rendered document.Siteis the builder call of the innermost statement the diagnostic falls in: a let, param, rule, assert, decision or invocation. A diagnostic in auseline names the firstReforInvokeof its path, and one in the header names theModuleorPolicycall.
deploy/tiers.sigil:5:9: error: `critical` is already the name of an enum value
|
5 | pub let critical = service.tier == critical
| ^^^^^^^^
= help: every name in a document means one thing; rename one of them
= go: vocabulary.go:35: build.PubErrors
type Error struct {
Msg string // what's wrong, one sentence without a trailing period
Site
}
type Errors []*Error
type Site struct {
File string // the Go file, as the runtime reports it
Call string // the builder function or method, as Go names it
Line int
}
func (e *Error) Error() string
func (es Errors) Error() string
func (s Site) String() string| Method or field | Formats |
|---|---|
Site.String() | vocabulary.go:35: build.Pub, with the base name of the Go file |
Site.Call | build.Field for a function, Expr.Matches for a method of Expr, (*PolicyDoc).When or (*Block).Decide for a statement method |
Error.Error() | vocabulary.go:35: build.Pub: message |
Errors.Error() | Every error on a line of its own, sorted by file and line |
- A document collects every error. Nothing stops at the first one, and no builder call panics.
- An error found when a call is made and one found when the document renders both carry the call's site.
Source on a module with seven mistakes:
vocabulary.go:71: build.Field: release.parent?.author reaches through an optional struct; read it with build.Opt, which renders `?.`
vocabulary.go:72: build.Opt: release.soak reaches through no optional struct; read it with build.Field
vocabulary.go:73: build.Ref: deploy.common has no pub let owner; it exports: owns_service
vocabulary.go:74: build.Ref: importing owns_service from team.common collides with owns_service imported from deploy.common; every name in a document means one thing
vocabulary.go:75: build.Lit: timestamp has no literal: a timestamp comes from input only
vocabulary.go:77: build.Not: an operand is the zero build.Expr; build one with build.Field, build.Lit or another constructor
vocabulary.go:78: Expr.Matches: invalid regular expression: error parsing regexp: missing closing ): `(`On a module with three mistakes in its fields and two in its options, WithPath(".hidden/bad.sigil") and WithPin(1) on a kind that accepts versions 2 and 3:
vocabulary.go:52: build.Opt: release.owner reaches through no optional struct; read it with build.Field
vocabulary.go:53: build.Field: fields release.a and release.b take no memory and share one address, so the pointer doesn't say which is meant; give their type a field, or read the one you mean with build.Raw
vocabulary.go:54: build.Opt: release.parent?.merged_by is optional itself, and optionals don't nest; read it with build.OptPtr
vocabulary.go:55: build.WithPath: ".hidden/bad.sigil" has an element that starts with `.`, which policy.Kind.Load skips, so the document would be silently left out; rename it
vocabulary.go:55: build.WithPin: kind DeployApproval accepts pins from version 2 to 3, not 1From FS with two documents whose paths differ only in case:
vocabulary.go:63: build.Module: deploy/env.sigil and Deploy/env.sigil render to paths that differ only in case; a file system that ignores case, like macOS's, keeps one of them, so give one another path with build.WithPath