go — schema-change-recipe
Detail page of the go recipe card.
How to change the charly.yml schema (CUE is the single source of truth)
Section titled “How to change the charly.yml schema (CUE is the single source of truth)”CUE (sdk/schema/*.cue — the sdk contract module) is the SOLE author-of-record
for the charly.yml ingress schema; the Go param structs in sdk/spec and the
reserved-word vocabulary are GENERATED / DERIVED from it (see “CUE is the single
source of truth”). The recipe:
- Edit CUE only. Add or change the field, kind, verb, or method enum in
sdk/schema/*.cue. A param-struct field is just a CUE field; a new KIND is a new#Nodearm + a per-kind#Def; a new VERB is a field on#Op(+ a#*Methodenum if it carries methods). Keep the#DefCLOSED (closed by DEFAULT — that is what catches a misspelled field); for two mutually-exclusive fields use a disjunction applied with&(#Box: {…} & ({from?: _|_} | {base?: _|_})), NEVER an embeddedmatchN, which silently disables closedness (the comments inbox.cue/candy.cue/vm.cuedocument this). - Annotate for Go with
@go(). A multi-word field →@go(GoName)(wire key preserved); a named scalar you want as a plain Gostring/map→@go(,type=string)merged as@go(GoName,type=string); a pointer / tri-state field →@go(GoName,optional=nillable)(→*T) or@go(GoName,type=*int|*bool); a disjunction field →@go(GoName,type=YourUnionType)and hand-write that union insdk/spec/union_types.go; a never-authored field →@go(-). NOTE: def-level@go(CharlyName)is BROKEN in cue v0.16.1 (it dangles the referencing fields) — expose a charly type NAME via a Go alias insdk/spec/charly_names.go(type BoxConfig = Box) instead. - Regenerate:
task cue:gen(in the sdk repo, or via the superproject task which chains the sdk generation first, then the per-plugin params loop). It runscue exp gengotypesintosdk/spec/cue_types_gen.go, the companionsdk/internal/schemagenintosdk/spec/vocab_gen.go+sdk/spec/version_gen.go, and the principled yaml-tag retag transform (both over thesdk/schemaconcatconcatenation). NEVER hand-edit the generated files (they carry theCode generated … DO NOT EDITbanner).TestGenReproducible(sdk/spec/gen_repro_test.go) fails if committed ≠ fresh. - Bind behavior. A new GENERIC install-VERB (a kernel primitive — rare) adds
ONE
VerbCataloghandler incharly/reserved_registry.gobinding the reserved word to its generated param type; the startupcheckVerbBijection(VerbCatalog, spec.OpVerbs, spec.AuthoringVerbs)gate panics fast (and failsTestReservedWordRegistry_*) if CUE and the registry disagree. A new KIND is NOT a core edit — it is a PLUGIN (per the kernel/plugin boundary law): it serves its own schema over Describe and is gated bycheckKindProviderBijection(spec.KindWords)(charly/provider_kind.go) againstspec.KindWords(empty; every authoring kind is plugin-served). See/charly-internals:plugin“The kernel/plugin boundary law”. - The drift gates (keep them all green). There is no
spec_parity_test.go; field parity is enforced three ways. (a) The compile-time alias surface — most authored param types are used fully-qualified asspec.Xdirectly (no charly-core alias at all); a shrinking residual subset still routes through charly’s OWN dissolvingcharly/*_aliases.gofiles (K4/K5 migration inventory — see “The alias surface” above; do not rely on this skill for which symbols currently route through which file, grep the actual file instead) — either way, a hand-referenced field that no longer has a matching spec field (name + wire-key + type) FAILS the build at that surface. (b)TestGenReproducibleproves the generated files match a freshtask cue:gen. (c) The reserved-word bijection gate proves the kind/verb/method wiring matches CUE. New kind also needs itssdk/schema/<kind>.cue#<Kind>def (reusing the shared defs in_common.cue) + a one-linecue_kind_<kind>.goregisterCueKindregistration + a corpus-test entry (cue_kinds_corpus_test.go). - Schema-version bump ONLY on an authored WIRE-key change. Only if the
change alters an authored WIRE key (the YAML users write) is it a FORMAT
change: then it is CROSS-REPO — bump
#SchemaVersioninsdk/schema/version.cue, runtask cue:gen(which regenerates theSchemaVersion/SchemaFloorconsts insdk/spec/version_gen.gothatkit.LatestSchemaVersion()parses), land + tag the sdk repo, then in the superproject bump the sdk submodule and append the matching entry to the declarative migration table (candy/plugin-migrate/migrations.cue— the TABLE lives in the compiled-incommand:migrateplugin) per/charly-build:migrate. A pure Go-identifier change via@go()is NOT a format change (wire key preserved) — do NOT bump the schema version. - Guards (all must pass):
cd charly && go test ./...(reproducibility + bijection + corpus + closedness + embedded-defaults viaTestEmbeddedDefaults_SchemaConformance) +charly box validateon the repo and everybox/<distro>submodule + the R10 bed gate.
Do NOT reintroduce a hand-maintained vocab list, a per-verb dispatch switch, or
a hand-written param struct — they are generated / derived from CUE now. Do NOT
use cue get go (that is the Go→CUE direction; CUE is the source here). The
ingress validation recipe is owned by /charly-build:validate; the egress
analog is the “Adding a new egress schema” recipe in /charly-internals:egress.