Repository navigation
feat(schemacompose): the one module-aware compose for a plugin's served schema - #297
Conversation
…ed schema opencharly/sdk#490's composer lives here, in the contract module, and the placement is decided by a rule rather than by taste: charly core may import github.com/opencharly/spec and may NOT import github.com/opencharly/sdk (charly's import-purity gate: "the sdk mechanism kits + the sdk root plugin-authoring package are for PLUGINS to import, never the host"), while the sdk and every plugin import this module. Both halves of the contract — the serve side and the host gate — must judge the same schema with the same code, so the ONE composer has to live where both of them may reach it. This module also already owns the family it belongs to: schemaconcat, schemaparams, cuetoolchain, and the embedded schema source plus the module declaration. ServedSchema composes a plugin's concatenated schema body as ONE CUE module instance: the plugin's own cue.mod/module.cue when its FS ships one, otherwise a declaration derived from this module's pinned language.version and naming github.com/opencharly/spec@v0 as its one dependency. ServedText composes the package-less body that travels on Capabilities.schema_cue — the same compose under the derived declaration, because the wire carries no module declaration, so the declaration must be one both sides derive. A modconfig.Registry serves the dependency from the embedded CUE module this Go module already carries, so the compose is offline: no network, no module cache, no cue.mod/pkg vendoring, and no local replace (the pinned modfile schema has no path field). The loader requires that dependency's source FS to implement module.OSRootFS, so the module is materialized ONCE at a content-named directory under the system temp root — never the host tree — and every compose shares it. The registry matches on the base path, since the loader asks with the major-suffixed path. The tests assert the four properties the cutover rests on: an importing schema composes and the imported definition is ENFORCED (a violating document fails with its own constraint error, a conforming one passes); a non-importing schema composes to the SAME value the text compile produced (canonical rendering identical, and the same verdicts over a document corpus); the two entry points agree byte for byte; and the compose is deterministic, refuses an unservable dependency and a foreign language.version, and is safe under concurrent use. opencharly/sdk#491 landed a first copy of this composer in the sdk (tag v0.2026313.433); the sdk's next unit makes its public entry points delegate here and deletes that copy, so there is ONE implementation. The host gate follows it. Assisted-by: DeepSeek Harness deepseek-v4.1-flash (fully tested and validated)
Review — BLOCKHead SHA: Change class: code — a new Go library package ( Blocks
Non-blocking notes (not gating): the deferred deletion of the Verdict: BLOCK ollama-cloud/deepseek-v4.1-flash — action-review. |
…n correctly The file header said the arms would PASS without the fix — the opposite of what it documents three lines later, of the PR body, and of R7 (coverage must FAIL without the change). It is the copied opening from the sdk's first draft, where the sentence was a typo from the start; the validator read it against its own surroundings and was right. The three arms it introduces are unchanged: the import arm FAILS on the pre-fix path at the first import clause, the equality arm is the regression guard, and the agreement arm is the byte-for-byte half. Assisted-by: DeepSeek Harness deepseek-v4.1-flash (fully tested and validated)
|
HOLDING — this PR holds up the two units that follow it, and both need its tag:
I am the same author on all three legs, so this is sequencing, not a handover. When this merges and tags, I post RCA of the BLOCK above (R1)
Agent: charly (DeepSeek Harness) Assisted-by: DeepSeek Harness deepseek-v4.1-flash (fully tested and validated) |
Review — PASSHead SHA: Security screen: PASS
Verdict: PASS ollama-cloud/deepseek-v4.1-flash — action-review. |
|
CLEARED — the hold this PR declared is cleared: it merged as Both units it was holding up are now unblocked and in flight:
Agent: charly (DeepSeek Harness) Assisted-by: DeepSeek Harness deepseek-v4.1-flash (fully tested and validated) |
…sdk delegates (#492) #491 put the module-aware compose for a plugin's served schema in this module. It cannot stay here: `charly/charly/import_purity_test.go` states the boundary as a tested invariant ("github.com/opencharly/sdk (root OR any subpackage) is for PLUGINS to import, never the host"), and both halves of the contract — the serve side and the host gate — have to judge the same schema with the same code. The ONE module both may import is github.com/opencharly/spec, which already owns this family (schemaconcat, schemaparams, cuetoolchain, the embedded schema source and its module declaration). So the composer moves to `spec/schemacompose` (opencharly/spec#297), and this module keeps ONE call site: `BuildCapabilitiesWithRequires` composes through it. The two public helpers this module added an hour ago, `ComposeServedSchema` and `ComposeServedText`, are DELETED rather than kept as wrappers — they carried no behaviour of their own, and a second copy of the entry points is exactly the duplication R2 exists to prevent. A plugin that wants them imports `spec/schemacompose` directly, as every plugin already imports this module's sibling packages from the contract module. The spec require moves to the release that carries the package. The acceptance arms that matter for this surface are kept here and re-stated where they now belong: a plugin whose schema imports the spec package is SERVED (it was refused before the cutover with "package imported but not defined"), a plugin that imports nothing is served unchanged, and the composer's own deep arms — the imported definition enforced, equality with the deleted text compile, the two entry points agreeing byte for byte — live in `spec/schemacompose`. Assisted-by: DeepSeek Harness deepseek-v4.1-flash (fully tested and validated)
Summary
The ONE module-aware compose for a plugin's served schema, in the contract module — the
shared half of opencharly/sdk#490.
Why here and not in the sdk.
charly/charly/import_purity_test.gostates it verbatim:github.com/opencharly/sdk(root or any subpackage) "is for PLUGINS to import, never thehost". The host may import
github.com/opencharly/spec; the sdk and every plugin importthis module. Both halves of the contract — the serve side and the host gate — must judge the
same schema with the same code, so the composer has to live where both may reach it. This
module already owns the family:
schemaconcat,schemaparams,cuetoolchain, the embeddedschema source and the module declaration.
ServedSchemacomposes a plugin's concatenated schema body as one CUE module instance:cue.mod/module.cuewhen its FS ships one, used verbatim after one check(its
language.versionmust be this module's);language.version, naminggithub.com/opencharly/spec@v0as its one dependency.ServedTextcomposes the package-less body that travels onCapabilities.schema_cue: theSAME compose under the derived declaration, because the wire carries no module declaration —
the declaration must be one both sides derive, which is what makes the two agree byte for
byte.
The dependency is served by a
modconfig.Registryfrom the embedded CUE module this Gomodule already carries, so the compose is offline: no network, no module cache, no
cue.mod/pkgvendoring, and no localreplace(the pinned modfile schema has no pathfield). The loader requires that dependency's source FS to implement
module.OSRootFS, sothe module is materialized once at a content-named directory under the system temp root —
never the host tree — and every compose shares it. The registry matches on the base path,
since the loader asks with the major-suffixed path.
Sequence. opencharly/sdk#491 landed a first copy in the sdk (tag
v0.2026313.433); thesdk's next unit makes its public entry points delegate here and deletes that copy, so there
is ONE implementation. The host gate — per-unit compose, then unify, in
charly/plugin_loader.go— follows it.Files
schemacompose/compose.goschemacompose/compose_test.golanguage.versionrefused, determinism, unservable dependency, concurrent useHow tested
This module's gate is its CI (
.github/workflows/ci.yml), run on the committed tree69207c795d0906efe6c6cded6e3ffa46e7728a83:The compose's own acceptance:
The unservable-dependency arm's message, which is the registry's boundary:
Measured cost, since this runs on the plugin-load path: a compose of a schema that imports
nothing is ~1 ms; one that imports this module is ~35–60 ms (the module load and build), paid
once per unit that carries an import.
Rulebook compliance
plugin,go,git-workflow,strict-policy) were loaded before the first edit, and this module's ownAGENTS.mdwasread first.
golangci-lintunable to read the hosttoolchain's export data) is the measured environment mismatch this repo's
AGENTS.mddocuments, and the lint was re-run under the toolchain
go.moddeclares.copy is deleted by the sdk unit that follows, not kept beside it.
errors naming the cause.
document fails with its own constraint error, a conforming one passes), the equality guard
compares against the deleted text compile, and the agreement arm compares the two entry
points.
run on the committed tree.
Change classification
Additive package in the contract module; no existing surface changes and no image or binary
is built here.
own units.
it until the following units land.
Filed issues
None.
Assisted-by: DeepSeek Harness deepseek-v4.1-flash (fully tested and validated)