| id | 236 | |
|---|---|---|
| title | cuelite phase 0 — package, façade, and differential harness | |
| status | ✅ | |
| model | opus | |
| summary | Create the public cue/cuelite package — the Value type, the CUE delegation pattern, the differential harness (in-house path versus the CUE-backed path as oracle), and the benchmark. Surface façade methods and call-site migration come in the per-surface phases that follow. | |
| depends-on |
|
Stand up the public cue/cuelite package as a CUE-backed
scaffold. Add the differential harness and benchmark the later
phases rely on.
Phase 0 of plan 218; see it for
the full design and strategy. The façade will mirror the CUE
calls mdsmith makes, each delegating to cuelang.org/go. Its
methods are added in the per-surface phases. Behaviour matches
CUE, so adopting it later stays green.
- Create
cue/cuelitewith itsValuetype (wrapping acue.Value), the CUE delegation pattern, and path-tagged errors. Phase 0 ships the minimal surface the harness and benchmark need —Compile,CompileJSON,Value.Unify,Value.Validate, and the package-levelErrorsaccessor. The per-surface façade methods (ParsePath,LookupPath,Decode, …) are added in their own phases. One unit test per function. - Build the differential harness: run a value or expression through the in-house path and the CUE-backed path, and assert identical accept/reject and error field-paths. There is no in-house path yet, so it starts as a scaffold.
- Add the
cue/cuelite-versus-CUE benchmark. - Register
cue/cuelitein the layering map.
-
cue/cueliteis a public, exported, documented package with itsValuetype and delegation scaffold. - Each function ships with a dedicated unit test.
- The differential harness and the benchmark run in CI.
- No mdsmith call site imports
cue/cueliteyet. - All tests pass:
go test ./... -
go tool golangci-lint runreports no issues.
Three choices differ from the sketch in plan 218:
- Per-
Valuecontext, not a shared package context. An earlier draft compiled everyValueagainst one package-wide*cue.Context. Code review rejected it: CUE v0.16.1 documents that values from oneContextare not safe for concurrent use and that long-lived contexts grow unbounded, so a single process-wide context is both a data race and a memory leak. Instead eachCompile/CompileJSONowns a fresh*cue.Contextand keeps its source bytes;Unifyrebuilds whichever side retains source into the OTHER side's context, so unification stays single-context and the result lives in the context of the side that was not rebuilt. That side's context is therefore MUTATED by the cross-contextUnify(the rebuilt operand is compiled into it), so twoValues sharing a context are not concurrency-safe and a long-livedValueaccumulates one compiled document per cross-contextUnify. This is the honest interim cost — one context per compiledValue, one rebuild of a cross-context operand perUnify, and the caller's duty to synchronize or per-goroutine-compile a shared schema. The flip to the in-house engine drops contexts entirely, with no API change:Valueis a value type whoseUnifytakes and returns aValue, and a bottom (⊥) absorbs in either implementation. - Harness lives in
internal/cuelitetest, not undercue/cuelite/. An earlier draft put it in a publiccue/cuelite/difftestsub-package. Code review rejected that too: the harness importscuelang.org/gofrom non-test files, so as a public package it would let external users depend on a package plan 218 phase 4 deletes, and it would pincuelang.orgintogo.modeven after the flip. Moving it underinternal/keeps it importable by every module test, invisible outside the module, and freely deletable. It exposesCueLitePath(the in-house path),OraclePath(the CUE oracle),Runover aCasecorpus, and a CI-visibleTestRun_corpus. EachOutcomecarries aStagediscriminator (compile-schema / compile-data / validate / accepted / error) so a schema the in-house engine cannot parse can never look like agreement with an oracle that merely rejected the data. CompileJSONenforces a strict-JSON, no-duplicate-keys contract, stricter than the plan-218 sketch's// CompileBytesannotation implied. CUE's JSON lift unifies same-named object keys into a phantom merged object that no last-wins JSON reader would build, so both arms reject any duplicate key before the lift —cuelitewith a parity-stack scanner, the oracle with an independent recursive walk. Both also defer lossy-decode keys (invalid UTF-8, lone-surrogate escapes) to the CUE lift rather than fabricating a duplicate, and surface a post-build bottom (a lone-surrogate value) as a data-stage compile error.
The phase-0 surface is small. It has Compile, CompileJSON,
Value.Unify, and Value.Validate, all on a value-type Value
that carries a bottom (⊥) for compile failures so a nil receiver
never panics. Validate returns one *PathError per failing leaf
(joined with errors.Join when several fail), each tagged with
its field path printed exactly once. The PathError type exports
Path and Error; its constructor is unexported, since no caller
outside the package builds one. The rest of the façade arrives in
the per-surface phases.
- The benchmark is gated, not just recorded.
TestFactorGateininternal/cuelitetestcomputes the cuelite/cue ns-per-op RATIO for the hot (BenchmarkValidate) and cold (BenchmarkCompileValidate) paths — minimum of five fixed-loop runs after a discarded warmup, so the ratio cancels runner noise the way it cancels runner speed (the benchcheck philosophy) — and FAILS when either exceeds its budget. Phase 0 shipped INTERIM budgets for the CUE-backed façade (HotFactorBudget2.5x,ColdFactorBudget2.0x — hot looser because the CUE-backed arm's cost was N-dependent). The plan-238 flip to the in-house engine TIGHTENED both to <= 1.0x (in-house must not be slower than the CUE oracle it replaces) — not deferred to plan 240. The ratio is only meaningful on a quiet runner — under the paralleltestjob's CPU contention the cuelite arm degrades more than the oracle arm and the factor inflates (observed 3.46x) — so the gate arms only whenCUELITE_FACTOR_GATE=1, set by the dedicatedcuelite-benchCI job, and skips everywhere else. The gate appends a factor table toGITHUB_STEP_SUMMARY. This makes plan 218's "the schema validate path does not regress" acceptance criterion enforceable; post-flip the measured factors sit near 0.26x (hot) and 0.34x (cold), well inside 1.0x.