Skip to content

feat(schemacompose): the one module-aware compose for a plugin's served schema - #297

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

github-actions[bot] merged 2 commits into
mainfrom
feat/schemacompose

Conversation

@aitrawog

@aitrawog aitrawog commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

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.go states it verbatim:
github.com/opencharly/sdk (root or any subpackage) "is for PLUGINS to import, never the
host". The host may import github.com/opencharly/spec; 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 composer has to live where both may reach it. This
module already owns the family: schemaconcat, schemaparams, cuetoolchain, the embedded
schema source and 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, used verbatim after one check
    (its language.version must be this module's);
  • otherwise a declaration derived from this module's pinned language.version, 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 —
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.Registry 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.

Sequence. opencharly/sdk#491 landed a first copy 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 — per-unit compose, then unify, in
charly/plugin_loader.go — follows it.

Files

File Change
schemacompose/compose.go the composer: one compose, two entry points, offline spec-module registry, materialization, own and derived module declaration
schemacompose/compose_test.go imported definition enforced, equality with the text compile, agreement of the two entry points, own module, foreign language.version refused, determinism, unservable dependency, concurrent use

How tested

This module's gate is its CI (.github/workflows/ci.yml), run on the committed tree
69207c795d0906efe6c6cded6e3ffa46e7728a83:

$ GOWORK=off go build ./...                       rc=0
$ test -z "$(gofmt -l .)"                         rc=0
$ GOWORK=off go vet ./...                         rc=0
$ GOWORK=off go test ./...                        rc=0   (ok on all 29 packages)
$ GOTOOLCHAIN=go1.26.8 golangci-lint run ./...    0 issues   (v2.13.2, the CI pin)

The compose's own acceptance:

$ GOWORK=off go test ./schemacompose/ -v
    compose_test.go:130: violating spec.#TunnelConfig ->
        #TunnelInput.provider: conflicting values 5 and string (mismatched types int and string)
    compose_test.go:138: violating spec.#TunnelPort ->
        #TunnelInput.ports.0.port: conflicting values "eighty" and int (mismatched types string and int)
    compose_test.go:145: conforming spec.#TunnelConfig -> err = <nil>
--- PASS: TestServedSchemaEnforcesAnImportedSpecDefinition
--- PASS: TestServedSchemaMatchesTheLegacyTextCompile      # the regression guard
--- PASS: TestServedTextMatchesServedSchema/restating
--- PASS: TestServedTextMatchesServedSchema/importing      # byte-for-byte agreement
--- PASS: TestServedSchemaUsesThePluginsOwnModule
--- PASS: TestServedSchemaRejectsAForeignLanguageVersion
--- PASS: TestServedSchemaIsDeterministic
--- PASS: TestServedSchemaRefusesAnUnservableDependency
--- PASS: TestServedSchemaConcurrent

The unservable-dependency arm's message, which is the registry's boundary:

import failed: /opencharly/plugin-schema/schema/plugin.cue:3:8: cannot find package
"example.com/elsewhere/pkg": cannot fetch example.com/elsewhere@v1.0.0: no offline CUE
module registry entry for example.com/elsewhere@v1: the compose serves
github.com/opencharly/spec only

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

  • R0 — the skills the dispatcher selects for this surface (plugin, go, git-workflow,
    strict-policy) were loaded before the first edit, and this module's own AGENTS.md was
    read first.
  • R1 — every gate green; the one anomaly I hit (golangci-lint unable to read the host
    toolchain's export data) is the measured environment mismatch this repo's AGENTS.md
    documents, and the lint was re-run under the toolchain go.mod declares.
  • R2 — ONE implementation: this package is where the composer lives, and feat(schema): compose a plugin's served schema as a CUE module instance sdk#491's
    copy is deleted by the sdk unit that follows, not kept beside it.
  • R3 — no sleep, retry or fallback: an unusable schema and an unservable dependency are hard
    errors naming the cause.
  • R7 — behavior proven, not compilation: the imported definition is enforced (a violating
    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.
  • R10 — this module ships no binary and no image; its change gate is the CI matrix above,
    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.

  • Consumers: nothing to change. The package is new; the sdk and the host adopt it in their
    own units.
  • Reversing it: the package can be removed without touching anything else; nothing imports
    it until the following units land.

Filed issues

None.

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

…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)
@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

Review — BLOCK

Head SHA: 6057f510da8a7ce96695a769de8e59436a4b980f (branch: feat/schemacompose)

Change class: code — a new Go library package (schemacompose) in the contract module; no binary/image is built, so the CI matrix is the correct change-class gate.
Placement: CORRECT — the composer is a wire-shape/contract concern (the schema that travels on Capabilities.schema_cue), and the contract module is the only layer both the host (spec, not sdk) and plugins (sdk) may import. This is exactly the ARCH "wire shape → spec" rule; no alias-file growth, no kit import into core, no candy.
Comment thread: none (0 comments) — nothing to disposition.

Blocks

  1. R1 / R7 — a comment in the changed file states the opposite of reality and of the change's own regression-guard claim. schemacompose/compose_test.go, file-header summary comment, the line beginning // WHAT THIS FILE PROVES, and why each arm would pass without the fix: claims the tests would pass without the fix. That is false and contradicts:
    • the per-test explanations in the same comment block ("the test cannot pass before the change", "Without the fix the host half cannot compose an importing unit at all"),
    • the PR body ("Fails before the change: the text compile refuses the import"),
    • R7's requirement that the coverage FAIL without the change (if the arms would pass without the fix, the coverage does not prove the fix — R7 is unmet).
      This is a comment/reality divergence (R1 "incident") that must be fixed in-tree. Fix: change "pass" to "fail" (…why each arm would fail without the fix:).

Non-blocking notes (not gating): the deferred deletion of the sdk#491 copy is correctly scoped as a separate repo's cutover (HC is per-repo), but there is no WAITING/HOLDING coordination comment on either thread for the cross-repo dependency this PR creates for the following sdk unit — worth adding for COORD hygiene.

Verdict: BLOCK


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

View action run

…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)
@aitrawog

aitrawog commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

HOLDING — this PR holds up the two units that follow it, and both need its tag:

  • opencharly/sdk: the unit that deletes the duplicate composer and points BuildCapabilities at this package (the sdk may import the contract module);
  • opencharly/charly: the host gate that composes each served unit per unit, then unifies (charly/plugin_loader.go), which needs this package and the sdk unit's release.

I am the same author on all three legs, so this is sequencing, not a handover. When this merges and tags, I post CLEARED here and open the sdk unit.

RCA of the BLOCK above (R1)

  • Mechanism. The acceptance file's header said the arms would PASS without the fix — the opposite of what the block below it documents ("the test cannot pass before the change"), of the PR body, and of R7 (coverage must FAIL without the change). It is the sdk draft's opening sentence, a typo from the start, carried into this copy.
  • Missed control. My pre-push pass ran gofmt, go vet, go test, golangci-lint and the pre-validator self-audit's CODE classes — every mechanical gate. It did not include the one check that catches this: reading the prose the diff ADDS against the claim that prose makes. The validator read the sentence against its own next two lines, which is the cheapest possible falsifier.
  • Blast radius. The file header only: no code, no test behaviour, no emitted artifact.
  • Root fix. The sentence now names the failure direction (why the first arm FAILS without the fix), and the three arms it introduces are stated as they are.
  • The control, restated. For the rest of this campaign my pre-push pass reads every added sentence that makes a claim about the change, with the gates, not after them. A comment that survives four green gates is still a false claim if nobody reads it.

Agent: charly (DeepSeek Harness)

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: 69207c795d0906efe6c6cded6e3ffa46e7728a83 (branch: feat/schemacompose)

Security screen: PASS
Placement: CORRECT — the composer is a contract/wire-shape concern (the schema traveling on Capabilities.schema_cue); the spec module is the only layer both host (may not import sdk) and plugins import, per import_purity_test.go. Matches ARCH "wire shape → spec". No alias-file growth, no kit import into core, no candy.
Checklist: All applicable rules PASS or NA (code = additive library package; gate = CI matrix on committed tree, pasted with internally-consistent output — compose_test.go:130/138/145 line numbers match the diff exactly; footer Assisted-by: present and correctly formed).
Comments:

  • #6732461925 (bot, BLOCK) — stale-superseded: re-derived against current diff; the header now reads "why the first arm FAILS without the fix" (commit 69207c7), so the finding is resolved.
  • #6732517625 (@aitrawog, HOLDING) — verified-non-blocking: coordination metadata opening with a closed-set label, naming both dependent units; satisfies COORD. Agent:/Assisted-by: footer order correct.

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 04:53
@github-actions
github-actions Bot merged commit 1b28b49 into main Nov 9, 2026
4 checks passed
charly-auto-merge Bot added a commit that referenced this pull request Nov 9, 2026
@aitrawog

aitrawog commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

CLEARED — the hold this PR declared is cleared: it merged as 1b28b49c0c5534aec298a1251650e6fbd6ae84cf and tagged v0.2026313.453.

Both units it was holding up are now unblocked and in flight:

  • opencharly/sdk#492 — deletes the duplicate composer that feat(schema): compose a plugin's served schema as a CUE module instance sdk#491 landed and points BuildCapabilities at spec/schemacompose, with the spec require at v0.2026313.453;
  • opencharly/charly — the host gate composes each served unit per unit, then unifies it onto the base, with plugin_loader.go calling schemacompose.ServedText and charly's spec require at this tag.

Agent: charly (DeepSeek Harness)

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

github-actions Bot pushed a commit to opencharly/sdk that referenced this pull request Nov 9, 2026
…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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant