Skip to content

feat(schemaparams): generate params in-process over the offline compose - #299

Merged
github-actions[bot] merged 2 commits into
mainfrom
feat/in-process-params
Nov 9, 2026
Merged

github-actions[bot] merged 2 commits into
mainfrom
feat/in-process-params

Conversation

@aitrawog

@aitrawog aitrawog commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

spec/schemaparams no longer shells out to cue exp gengotypes. The schema directory is concatenated as before, then composed as ONE CUE module instance through schemacompose — the same offline load.Config + registry the serve side and the host gate already use — and the Go params are generated in-process from the CUE Go API. Generation and composition now share one resolution path, and the cue CLI leaves the generation leg entirely.

Both measured failures in #298 are closed:

  • F1 — nothing resolved the import. schemacompose gained an exported ComposeText that returns the loaded module instance, so the projection composes through the ONE offline registry instead of writing a scratch module and shelling out. A plugin schema carrying import "github.com/opencharly/spec/schema" now loads against the contract module the Go binary already links: no published CUE module, no module cache, no cue binary.
  • F2 — the emitted Go named a package that does not export the type. A referenced CUE package is now emitted through the Go package that owns its shapes: github.com/opencharly/spec/schema maps to github.com/opencharly/spec/spec (schemaparams/gohome.go). An import with no registered Go home is a loud error, never a reference to a path that may not compile.

The generator (schemaparams/gotypes.go) is a faithful in-process port of cue exp gengotypes v0.16.1 — that package is INTERNAL to cuelang.org/go, which is why the CLI was shelled out to — so a schema without an import generates byte-identically; both directions are proved below. The one thing not delegated to the local formatter is doc-comment canonicalization: Go 1.27's go/printer stopped rewriting a struct field's doc comment, so leaving it to the toolchain would make the same schema regenerate differently on a 1.27 developer box and in the 1.26 CI. The transform now uses the public go/doc/comment parser, and generation is byte-identical under both Go 1.26.8 and Go 1.27.3.

The committed spec/cue_types_gen.go is unchanged: a clean regeneration is a no-op, and TestGenReproducible now compares bytes on every run (it no longer skips when the CLI is absent). The next leg of opencharly/opencharly#950 is the consumer call site in opencharly/plugin-candy (see Change classification); the census is opencharly/opencharly#945.

Head 20baf6439660df9f1d69e1625c4da71b11cac0e5.

How tested

Every command below ran from the spec worktree on the committed tree (git status --short empty) at Head 20baf6439660df9f1d69e1625c4da71b11cac0e5.

The gate this repo's CI declares

$ GOWORK=off go build ./...
rc=0

$ GOWORK=off go test ./... -count=1 > /tmp/spec-test.log 2>&1; echo "rc=$?"
rc=0

$ grep -c '^ok ' /tmp/spec-test.log
29

$ grep -E '^(FAIL|--- FAIL)' /tmp/spec-test.log || echo '(no failures)'
(no failures)

$ gofmt -l .
(no output)

$ GOWORK=off GOTOOLCHAIN=go1.26.8 golangci-lint run ./...
0 issues.
rc=0

No-import case: byte-identical to the pinned cue exp gengotypes

The oracle inside schemaparams_test.go re-runs the old recipe verbatim with the pinned CLI (concat, scratch module, cue exp gengotypes, tag normalization) and the test compares the two outputs byte for byte:

$ GOWORK=off go test ./schemaparams/ -run TestGenerateNoImportIsByteIdenticalToGengotypes -count=1 -v
=== RUN   TestGenerateNoImportIsByteIdenticalToGengotypes
--- PASS: TestGenerateNoImportIsByteIdenticalToGengotypes (0.03s)
PASS
ok  	github.com/opencharly/spec/schemaparams	0.044s
rc=0

The same comparison outside the test, on the plugin-tunnel twin shape (no import), plus the two-toolchain comparison (go1.27.3 output vs go1.26.8 output):

$ cmp /tmp/pr-evidence/twin-inproc.go /tmp/pr-evidence/twin-gengotypes.go
cmp rc=0
e7fd28681c143893cb056702d83e03eaa52df8b67b83e7fc1e49fb4f62712551  /tmp/pr-evidence/twin-inproc.go
e7fd28681c143893cb056702d83e03eaa52df8b67b83e7fc1e49fb4f62712551  /tmp/pr-evidence/twin-gengotypes.go

$ cmp /tmp/ev127/twin.go /tmp/ev126/twin.go
cmp rc=0
$ cmp /tmp/ev127/import.go /tmp/ev126/import.go
cmp rc=0

/tmp/pr-evidence/twin-inproc.go is schemaparams.Generate; /tmp/pr-evidence/twin-gengotypes.go is the pinned CLI's output on the same source. /tmp/ev127 and /tmp/ev126 hold the same generated files from the two toolchains.

spec's own generation: clean regeneration is a no-op

$ GOWORK=off go run ./internal/schemagen -mode=params -schema=schema -pkg=spec -out=/tmp/pr-evidence/spec-fresh.go
rc=0

$ diff -q spec/cue_types_gen.go /tmp/pr-evidence/spec-fresh.go
rc=0

$ sha256sum spec/cue_types_gen.go /tmp/pr-evidence/spec-fresh.go
ba1fa57523e1fb1daaad166c9aea58681bc7501026a02efcce03beaa37dd5e3a  spec/cue_types_gen.go
ba1fa57523e1fb1daaad166c9aea58681bc7501026a02efcce03beaa37dd5e3a  /tmp/pr-evidence/spec-fresh.go
$ GOWORK=off go test ./spec/ -run TestGenReproducible -count=1 -v
=== RUN   TestGenReproducible
--- PASS: TestGenReproducible (0.46s)
PASS
ok  	github.com/opencharly/spec/spec	0.464s
rc=0

The real blocked case: plugin-tunnel's import shape generates usable Go

The fixture is the prior measurement's shape B (schemaparams/testdata/import/schema/tunnel.cue): import "github.com/opencharly/spec/schema", the twins #TunnelConfig/#TunnelPort deleted, #TunnelInput referencing schema.#TunnelConfig. The generated file, and the build of it in a module that requires this one:

$ cat params/cue_types_gen.go
// Code generated by "cue exp gengotypes"; DO NOT EDIT.

package params

import (
	"github.com/opencharly/spec/spec"
)

// #TunnelInput — the plugin_input shape for the `tunnel` verb.
type TunnelInput struct {
	// method — the tunnel operation: start | stop | setup | plan. `plan` is the
	// creds-free dry-run that returns the argv the operation WOULD run (no exec).
	Method string `yaml:"method,omitempty" json:"method"`

	// config — the resolved tunnel configuration to act on (byte-compatible with the
	// core's TunnelConfig, sent by the calling pod-lifecycle plugin over the Invoke envelope).
	Config spec.TunnelConfig `yaml:"config,omitempty" json:"config,omitempty"`

	// expect — plan only: the expected argv command lines (space-joined). When set, the
	// plan method compares the built argv against it (FAIL on mismatch); empty ⇒ the
	// step passes as long as dispatch + round-trip succeed, echoing the built argv.
	Expect []string `yaml:"expect,omitempty" json:"expect,omitempty"`
}

$ cat go.mod
module example.com/plugin-tunnel-params

go 1.26.0

require github.com/opencharly/spec v0.0.0

replace github.com/opencharly/spec => /home/atrawog/Sync/Atrapub/lo/dsh/opencharly/.worktrees/dsh-271/spec

$ GOWORK=off GOFLAGS=-mod=mod GOPROXY=off go build ./params
go build rc=0

$ GOWORK=off GOFLAGS=-mod=mod GOPROXY=off go vet ./params
go vet rc=0

Coverage added by this change

$ GOWORK=off go test ./schemaparams/ -count=1 -v
=== RUN   TestGenerateNoImportIsByteIdenticalToGengotypes
--- PASS: TestGenerateNoImportIsByteIdenticalToGengotypes (0.02s)
=== RUN   TestGenerateImportReferencesTheOwningGoPackage
--- PASS: TestGenerateImportReferencesTheOwningGoPackage (0.05s)
=== RUN   TestGenerateImportCompiles
--- PASS: TestGenerateImportCompiles (0.18s)
=== RUN   TestGenerateHonoursThePackageName
--- PASS: TestGenerateHonoursThePackageName (0.00s)
=== RUN   TestGenerateExcludingHonoursTheFilter
--- PASS: TestGenerateExcludingHonoursTheFilter (0.00s)
=== RUN   TestGenerateRejectsAnEmptySchemaDir
--- PASS: TestGenerateRejectsAnEmptySchemaDir (0.00s)
=== RUN   TestGoImportForRejectsAnUnregisteredPackage
--- PASS: TestGoImportForRejectsAnUnregisteredPackage (0.00s)
=== RUN   TestGoImportForUsesTheGoHome
--- PASS: TestGoImportForUsesTheGoHome (0.00s)
PASS
ok  	github.com/opencharly/spec/schemaparams	0.269s
rc=0

Update after the first CI run: the compile arm's module graph

The first CI run failed the go job in TestGenerateImportCompiles (verdict PASS on the code; this was a test-harness defect). RCA: the temp module was declared with a minimal require spec and built with -mod=mod, so go build tried to EXPAND the module graph and asked the proxy for gopkg.in/check.v1's go.mod, which CI's cache did not hold; GOPROXY=off refused the lookup. It passed locally only because this host's cache happened to hold that go.mod. The missed control was the arm's own declaration: a consumer module has a COMPLETE requirement set, not a minimal one. The arm now mirrors this module's declaration (same go directive and requirement set, module path rewritten, contract module added as a local replace) and builds in the default readonly mode, so no expansion is attempted. Reproduced with a cold cache minus check.v1:

# cold module cache with gopkg.in/check.v1 removed (the CI condition)
# the OLD construction (minimal require + -mod=mod):
go: gopkg.in/yaml.v3@v3.0.1 requires
	gopkg.in/check.v1@v0.0.0-20161208181325-20d25e280405: module lookup disabled by GOPROXY=off
old construction rc=1

# the NEW construction (the module declaration mirrored, readonly):
$ GOMODCACHE=/home/atrawog/.cache/modcache-cold GOWORK=off go test ./schemaparams/ -run TestGenerateImportCompiles -count=1 -v
=== RUN   TestGenerateImportCompiles
--- PASS: TestGenerateImportCompiles (0.21s)
PASS
ok  	github.com/opencharly/spec/schemaparams	0.223s
new construction rc=0

Rulebook compliance

  • R0 — the dispatched skills were loaded before the first tool action: git-workflow, go, plugin and strict-policy, plus this repo's AGENTS.md and the umbrella rulebook read from git show origin/main.
  • R1 — two anomalies surfaced and were root-fixed. (1) The in-process output was 64 bytes longer than the pinned CLI's: Go 1.27's go/printer gained tok != token.IDENT in its doc-comment rewrite gate, so struct-field doc comments were no longer canonicalized; the missed control was an arm comparing the new generator against the CLI. The fix canonicalizes doc comments in-process with go/doc/comment, and the oracle arm plus the two-toolchain comparison hold the line. (2) The first CI run's go job failed in the compile arm's temp-module declaration (RCA and cold-cache reproduction above); the fix mirrors the module's own declaration so no graph expansion is attempted. No finding is deferred; the lint reports 0 issues.
  • R2 — one compose (schemacompose.composeServedBody, shared by ServedSchema, ServedText and ComposeText), one generator, one tag normalizer, and one CUE-to-Go relation (goHomes); no second resolver was written.
  • R3 — no sleeps, retries or suppressions; an unresolvable import, an empty schema directory and an import with no Go home each fail with a named error; no CLI is provisioned or executed on the generation leg.
  • R4 — the product was fixed first (generator, compose, relation) and the prose follows: schema/README.md, charly.yml and the package docs describe the shipped behaviour.
  • R5 — the deleted identifiers are swept; the remaining mentions are the mapping semantics the port reproduces, explained in schema/README.md:
$ grep -rn 'GenerateWithCue' --include='*.go' .
rc=1

$ grep -rn 'cueDir' --include='*.go' .
rc=1

$ grep -rln 'gengotypes' --include='*.go' --include='*.yml' --include='*.md' . | grep -v '^./CHANGELOG/'
./schema/README.md
./schemaparams/schemaparams.go
./schemaparams/schemaparams_test.go
./schemaparams/gotypes.go
./schemaretag/schemaretag.go
./schemaretag/schemaretag_test.go
./spec/arbiter_consts.go
./spec/build_resolved_box.go
./spec/builder_config_methods.go
./spec/charly_names.go
./spec/cue_types_gen.go
./spec/deploy_consts.go
./spec/deploy_methods.go
./spec/distro_config_methods.go
./spec/distro_methods.go
./spec/enc_consts.go
./spec/hand_state_types.go
./spec/host_context.go
./spec/ir_enums.go
./spec/libvirt_gpu_surface_test.go
./spec/libvirt_gpu_vocab_cue_test.go
./spec/loader_seam.go
./spec/pipeline_test.go
./spec/resolved_box.go
./spec/scalar_aliases.go
./spec/stage_parallel_cue_test.go
./spec/status_result.go
./spec/status_types.go
./spec/substrate_template_methods.go
./spec/union_types.go
  • R6 — the worktree was created off fresh origin/main (20f14cf5e383ed284b3d579fb47f7f09b9209eb9), git status --short was empty before each commit, and the branch is pushed for a pull request only: no force-push, no direct push to main.
  • R7 — the new coverage fails without the change: the import arms cannot run on the old shell-out (the CLI cannot resolve the import) and the oracle pins every byte of the no-import output; the generated file is compiled at its real boundary (go build, not a string assertion), re-proved against a cold module cache.
  • R8 — N/A — no live service boundary is crossed; the compose serves the embedded contract module offline, so there is nothing to mock or skip.
  • R9 — N/A — this repo ships no binary or runtime OS dependency; the Go library is tested at the toolchain its go.mod declares.
  • R10 — N/A — this contract module declares no disposable bed and ships no image; the CI gate it declares (build, test, gofmt, lint) ran on the committed tree above, and the org-wide charly/pr-validator is the merge gate.

Change classification

  • Consumer-visible API change. schemaparams.Generate(schemaDir, pkg, cueDir) becomes Generate(schemaDir, pkg), GenerateExcluding loses its cueDir argument, and the GenerateWithCue pair is deleted. The one call site is candy/plugin-candy/params.go in opencharly/plugin-candy, whose paramsPipeline(schemaDir, pkg, cueDir) call and CLI-provisioning help text change with its pin bump — the next leg of the producer-first sequence in design: a duplicate row retires only with the composition — the one-home half of D-PAT-2 is a mirror the text splice forces opencharly#950. Nothing in this repo consumes the removed surface.
  • No authored wire key changes. spec/cue_types_gen.go and spec/vocab_gen.go are byte-identical to the committed files; a schema without an import generates exactly what it generated before.
  • Generated output for an importing schema changes by design. The file now imports github.com/opencharly/spec/spec and references its types, instead of importing the CUE package path whose Go package exports only FS.
  • The generated-file banner is deliberately unchanged. Byte-identity for the no-import case is the no-regression criterion, and changing the banner would rewrite every committed params file across the org for no semantic gain; the generator is a faithful port of the mapping the banner names, and schema/README.md states that reading.
  • The cue CLI is off the generation leg, not out of the repo. bootstrap-cue stays as the pinned tool for authoring the schema as a module (cue eval ./schema); cue-gen no longer depends on it.
  • Going back: re-pin the previous released contract module, which still contains the CLI pipeline in schemaparams.

Filed issues

Closes #298. Updates #271.

Files changed

File Change
charly.yml cue-gen no longer depends on bootstrap-cue; comments describe the in-process generation and the remaining authoring role of the CLI
cue.mod/module.cue language-version rationale now points at the go.mod library pin
cuetoolchain/cuetoolchain.go its role is the pinned authoring CLI, not the generation leg
internal/cuesetup/main.go same role correction
internal/schemagen/main.go params mode calls the two-argument in-process pipeline; comments updated
schema/README.md pipeline description rewritten; the remaining gengotypes mentions explained as the mapping the port reproduces
schemacompose/compose.go exported ComposeText and Instance; the one compose now feeds the projection too
schemaconcat/schemaconcat.go comment names the in-process generator
schemaparams/gohome.go new: the CUE package to Go package relation that closes F2
schemaparams/gotypes.go new: the in-process generator, a faithful port of cue exp gengotypes v0.16.1 with local doc-comment canonicalization
schemaparams/schemaparams.go compose plus in-process generation, two-argument API
schemaparams/schemaparams_test.go pinned-CLI oracle, import reference and compile arms, empty-dir and filter arms, hermetic temp-module declaration
schemaparams/testdata/import/schema/tunnel.cue the shape-B fixture that blocked the consumer unit
schemaparams/testdata/twin/schema/tunnel.cue the twin fixture used for byte-identity
schemaretag/schemaretag.go doc names schemaparams as its caller
spec/gen_repro_test.go no CLI gate (generation is in-process); the gate compares bytes on every run

Assisted-by: DeepSeek Harness deepseek-v4.1-flash (fully tested and validated)

Replace the `cue exp gengotypes` shell-out with charly's own CUE-Go-API
generator, loaded through schemacompose's offline module registry, so a
plugin schema that imports github.com/opencharly/spec/schema projects to
the Go package that owns the shapes (github.com/opencharly/spec/spec).

- schemacompose exports ComposeText and Instance, so the ONE compose now
  feeds the serve side, the host gate and the Go projection from the same
  module instance and the same offline resolution.
- schemaparams/gotypes.go ports gengotypes v0.16.1's mapping in-process.
  A schema without an import stays byte-identical: the pinned-CLI oracle
  in the tests compares byte for byte, and the committed spec tree
  regenerates as a no-op. Doc-comment canonicalization is done locally so
  Go 1.26 and Go 1.27 emit one form.
- schemaparams/gohome.go carries the CUE-package to Go-package relation
  that closes F2; an import with no registered Go home is a loud error.
- cue-gen no longer bootstraps the cue CLI: the CLI leaves the generation
  leg entirely and remains the pinned authoring tool for `cue eval ./schema`.

Generate(dir, pkg) replaces Generate(dir, pkg, cueDir); the consumer call
site in opencharly/plugin-candy is the next leg of opencharly/opencharly#950.

Closes #298. Updates #271.

Assisted-by: DeepSeek Harness deepseek-v4.1-flash (fully tested and validated)
@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

Review — PASS

Head SHA: 47738b7067571a475f304f9072a29d1470dc6c01 (branch: feat/in-process-params)

Security screen: PASS — no injection directives, no secrets, no added egress, no weakened guardrails (the CLI is removed from the generation leg, a net reduction); no scope mismatch (all 16 files accounted for in the body).
Checklist: All applicable rules PASS or NA — A1/A2/A3 (pasted gate output + byte-identity oracle + real go build of the import case justify fully tested and validated), R1 (64-byte anomaly root-caused with the missed control named), R5 (grep output internally consistent with the diff), R8 (oracle uses the real pinned CLI and skips visibly), R10 coverage (import arms fail without the change), HC (one commit). Placement: CORRECT — the generator and the CUE→Go home relation live in the shared schemaparams mechanism package consumed by both spec's own generator and the charly-side verb; no capability code moved into core, no alias file, no kit import.

Verdict: PASS


ollama-cloud/deepseek-v4.1-flash — action-review.

View action run

The compile arm built its temp module from a minimal `require spec` and
`-mod=mod`, so `go build` expanded the module graph and asked the proxy for
gopkg.in/check.v1's go.mod — which CI's cache did not hold, and GOPROXY=off
refused the lookup:

  go: gopkg.in/yaml.v3@v3.0.1 requires
      gopkg.in/check.v1@v0.0.0-...: module lookup disabled by GOPROXY=off

It passed locally only because this host's cache happened to hold that go.mod.
The temp module now mirrors this module's own declaration (same go directive,
same requirement set, module path rewritten, contract module added as a local
replace) and builds in the default readonly mode, so no graph expansion is
attempted. Reproduced with a cold cache minus check.v1: the old construction
fails with the CI error, the new one passes.

Assisted-by: DeepSeek Harness deepseek-v4.1-flash (fully tested and validated)
@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

Review — PASS

Head SHA: 20baf6439660df9f1d69e1625c4da71b11cac0e5 (branch: feat/in-process-params)

Security screen: PASS — no injected directives, no secrets/egress, no new deps; the shell-out to the checksum-pinned CLI is removed from the generation leg (net reduction), the pin/fetcher is preserved for authoring/tests, and no security machinery (validator spec, rulebook, hooks, branch protection) is touched.
Checklist: All applicable rules PASS or NA — A1 (16/16 files accounted; pasted gate output, oracle, sha-equality and real go build of the import case), A2/A3 (fully tested and validated justified: the new generator + import path executed and the generated file compiled at its boundary; docs-only rules not claimed), R1 (both anomalies — the 64-byte doc-comment drift and the CI compile-arm graph expansion — root-caused with the missed control named and fixed in-tree, cold-cache reproduced), R5 (grep sweep pasted and consistent with the diff; remaining gengotypes mentions explained as the reproduced mapping), R7/R10 coverage (import arms and oracle would fail without the change; library repo declares no bed, N/A reasoned), HC (one cutover + one fix commit), ARCH placement CORRECT (generator and CUE→Go home relation stay in the shared schemaparams/schemacompose mechanism packages consumed by spec's generator and the charly-side verb; nothing moves into core, no alias growth, no kit import).
Comment considered: Comment 6742254056 by @github-actions[bot] — stale-superseded: it reviewed head 47738b70; the current head 20baf64 adds the hermetic compile-arm fix, so the PASS is not carried forward as authority, though its substantive claims (placement, R1, R5) re-derive correctly against the current diff.

Verdict: PASS


ollama-cloud/deepseek-v4.1-flash — action-review.

View action run

@github-actions
github-actions Bot enabled auto-merge (squash) November 9, 2026 14:26
@github-actions
github-actions Bot merged commit 9428017 into main Nov 9, 2026
4 checks passed
charly-auto-merge Bot added a commit that referenced this pull request Nov 9, 2026
github-actions Bot pushed a commit to opencharly/plugin-candy that referenced this pull request Nov 9, 2026
… generation leg (#13)

spec#299 replaced the `cue exp gengotypes` shell-out with an in-process CUE-Go-API
generator. `schemaparams.Generate` now takes (schemaDir, pkg); the cueDir argument
and `GenerateWithCue*` are gone.

- params.go: call the two-argument Generate, delete cueCacheDir and the
  cuetoolchain import. The verb's own wiring (path derivation, write-on-change
  no-op, --check) is unchanged.
- params_test.go: the pipeline stub and the real-pipeline arm follow the new
  signature. Generation is in-process, so the provisioning skip is deleted and a
  failure is a failure rather than a visible skip.
- .agents/skills/candy/SKILL.md and the paramsUsage text: the schema is composed
  and generated in-process (no cue CLI, no checkout, no toolchain provisioning);
  the pinned cue CLI remains the authoring tool for schema sources.
- go.mod/go.sum: spec v0.2026281.933 -> v0.2026313.1428 (tag v0.2026313.1428
  contains merge 9428017f). The spec bump also requires sdk v0.2026280.2246 ->
  v0.2026309.2334, the version charly already pins: the older sdk's vmshared
  aliases reference spec types removed by the intervening schema changes, so the
  module does not compile against the new spec without it.

Refs opencharly/spec#299, opencharly/opencharly#950, opencharly/opencharly#945.

Assisted-by: DeepSeek Harness deepseek-v4.1-flash (fully tested and validated)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant