Skip to content

fixture-composition fragments merged by the run, and hop-routes delivered where no module is named - #1197

Merged
antoinetoussaint-byte merged 10 commits into
mainfrom
generate/fixture-composition
Oct 27, 2026
Merged

antoinetoussaint-byte merged 10 commits into
mainfrom
generate/fixture-composition

Conversation

@antoinetoussaint-byte

@antoinetoussaint-byte antoinetoussaint-byte commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Two changes to one repo, one PR.

1. fixture-composition is merged by the platform

The local authority's fixture-composition document is merged from one fragment per composed module, never typed per playground.

  • pkg/fixturefragments: finds each workspace module's configurations/<profile>/fixture-composition.yaml (profile chain resolved by core's configurations.ProfileDirectory, in the module's own directory), merges them with the authority's library (codefly-dev/authority/libraries/fixture-composition/go: one source per (org, role, kind) grant / installation / stand-in, conflicts refused naming both fragments), and writes the workspace's configurations/<profile>/fixture-composition.yaml with a generated marker and the list of sources.
  • codefly run (newRunFlow, before the flow reads any configuration — the same seam as the generated service-secrets): writes it when a composed service declares the group. No Makefile step.
  • codefly generate fixture-composition --env local: the same, on its own (materializes pinned modules first, like a run).
  • Hand-written file: never overwritten. generate refuses it by name; run reads it as it stands and warns that no module's fragment is applied, which keeps the not-yet-migrated playgrounds (authority/playground, core-solutions wiki/proposals) booting.
  • docs/cli-reference.md regenerated.

Reuse: core already delivers each composed module's configurations/<profile>/*, and a workspace's own document already resolves an ambiguity between composed modules (composeModuleWorkspaceConfigurations). The generated file uses exactly that precedence, so core is unchanged and the authority's reader is unchanged. The merge rules are the schema owner's library, which this repo already depends on.

2. hop-routes is delivered empty where no module is named

A host that stands a hop on some compositions and not others declares hop-routes always — saas's auth-gateway is the hop on a cell and stands none where the composition names no module, which its own service.codefly.yaml documents: "A local run composes the authority module's hop instead and states no hop/MODULES for this service." generatedHopRoutes returned no group at all for that composition, and core's GetWorkspaceDependenciesConfigurations refuses a declared group that resolves to nothing: the service failed at runtime-init, from the declaration, before any of its code ran, so it could not degrade — and it could not stop declaring the group either, since generation requires a declaring service. This blocks saas #1265.

The group is now delivered with an empty ROUTES. Verified safe:

  • nothing decodes it — the gateway stands its hop only where hop/MODULES names modules, and freezeDeliveredHopAddresses skips an empty value, so routing.DecodeRouting is unreachable;
  • the routing snapshot of an empty value equals that of no group, so no run is told its routes changed;
  • the declaring-service check runs before the empty return, so a workspace no service of which declares the group still receives nothing;
  • a composition that names modules still receives its generated routes, unchanged.

Why an empty value satisfies the resolver at all: core's Manager.GetWorkspaceDependenciesConfigurations keys group presence by name, not by content.

Why the empty string, specifically. routing.Validate requires at least one endpoint, and authority's own work_test.go asserts that even {"schema":…,"endpoints":[]} is refused. There is deliberately no "no routes" document in hop-routes/v1, so the empty string is the only available spelling for "no table". If that should be hardened, the fix belongs at authority — whether the schema gains a legitimate empty document — and it is a follow-up, not this PR.

The shape I considered and rejected: OptionalWorkspaceConfiguration

The alternative is to leave generatedHopRoutes returning nothing and add hop-routes to environments.OptionalWorkspaceConfiguration, whose doc comment states this contract word for word: a service that declares such a group reads the keys it needs and treats an absent one as not configured, so every path that resolves a service's declared groups delivers nothing for one the workspace does not provide. Both hop consumers do read their group that way — saas's standHop reads hop/MODULES with the tolerant accessor and returns before it mentions hop-routes, and the authority module's own hop refuses by name on the same read. On the reuse rule alone that is the better-looking fix, and I built it before rejecting it.

It is rejected because that seam is two halves and only the run half is generic. checkRequiredWorkspaceConfiguration dispatches every optional group into checkRunnableBindingsGroup, which calls generate.SelectRunnableBindings unconditionally and judges the group against the runnable bindings the environment selects. Added to that list as it stands, hop-routes gets judged about runnable bindings:

  • locally, status=ok with environment local prepares no runnable binding, so "hop-routes" holds none — green about an entirely different thing;
  • in a workspace that declares hop-routes and selects runnable bindings, held is empty so every selected binding key counts as missing and the doctor emits a spurious hard configuration_missing: "workspace configuration "hop-routes" lacks N runnable binding(s) environment local selects".

Making it right means splitting that dispatch and writing a checkHopRoutesGroup — more work than this fix and a change to a shared seam runnable-bindings depends on. The two shapes are behaviourally equivalent at both known consumers (the refusal was never raised in gateway code; both make the declaration resolve), so the doctor defect is the only thing separating them.

Can a composition that should name modules resolve to nothing and fail later?

No. The group is delivered only where a service declares it, and the empty table only when hop/MODULES is empty — a documented, legitimate composition. Where modules are named nothing is weakened: an uncomposed module, an unreadable catalog or a disagreeing principal still fails the reader, which codefly doctor workspace reports as configuration_invalid ("not checked"), and a new case in the refusal test proves it.

What neither shape can catch is an operator who meant to name modules and left hop/MODULES empty: to the CLI that is indistinguishable from "stand no hop". That was equally true before — the old behaviour did not catch the mistake, it crashed every composition that legitimately stands no hop.

The doctor is the same seam, not a second fix

checkConfigurationSources already takes generated groups from this inventory — its own comment says "never from a name exemption" — so once the group is delivered the doctor reports it as supplied by the invocation instead of configuration_missing. No name list was touched.

One deliberate test change. TestDoctorWorkspaceStillRefusesInvalidHopInputs carried {"no selected modules", …, codeConfigurationMissing, "hop-routes"}, asserting that this composition must fail the doctor. That was the symptom, not a rule — the consumer repo documents the composition as legitimate — so it is replaced by TestDoctorWorkspaceAcceptsAHopThatSelectsNoModule rather than kept green. The sibling {"missing ordinary group", …} case still proves the doctor reports a genuinely missing non-generated group, so the weakening is scoped.

Evidence

  • go test ./pkg/fixturefragments/ ./pkg/orchestration/ ./cmd/ ./cmd/run/ ./cmd/generate/ green. Fragment merge of the authority's, two modules' and the playground's; conflict naming both fragments; hand-written file kept; nothing declared without a declaring service; the run hook writes before load. For hop-routes: the empty delivery for an empty and an absent hop group, a composition naming modules still generating routes, and a workspace declaring nothing still receiving nothing.
  • golangci-lint run (v2.14.0, make lint) clean on the changed packages.
  • Live with this branch's binary, module-document-store playground (authority de0deb68): the run printed merged …/configurations/local/fixture-composition.yaml from 3 fragment(s), the authority logged the same 8 grants as before except Maya's collection read is now id: derived\x1fdocuments-playground; Maya then lists Rémi's document in that collection (200), and every other verified call is unchanged. Robin's playground: see obin-ai/module-robin#562.

Fix or hack

Both are fixes at the owner: the platform generates the fixture document from its one source per module, and the CLI delivers the group its own generation owns.

🤖 Generated with Claude Code

antoinetoussaint-byte and others added 4 commits October 27, 2026 10:44
… before the local authority boots

Each module states once, next to its code, what the local authority's standard
fixture roles may do at its kinds (configurations/<profile>/fixture-composition.yaml
in the module); a composition's own module states only what is its own. `codefly
run` now merges those fragments into the workspace's fixture-composition document
before anything reads the configuration, when a composed service declares the
group, and `codefly generate fixture-composition --env local` does the same on
its own. The merge rules are the authority's
(codefly-dev/authority libraries/fixture-composition): every grant on one
(organization, role, kind), installation and stand-in module has one source, and
a second is refused naming both. The written file says it is generated; a
hand-written one is never overwritten (generate refuses it; a run reads it as it
stands and warns that no module's fragment is applied).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… as a run does

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
golangci-lint v2.14.0 (`make lint`): the generated document is written 0600
like every other file the run writes, the three shadowed `err` declarations are
named for what they report, and the unused cobra parameter is `_`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
antoinetoussaint-byte and others added 5 commits October 27, 2026 11:35
…ed gets the group empty, not nothing

A host that stands a hop on some compositions and not others declares
`hop-routes` always — saas's auth-gateway is the hop on a cell and stands none
where the composition names no module. `generatedHopRoutes` returned no group
at all for that composition, and core's reader
(GetWorkspaceDependenciesConfigurations) refuses a declared group that resolves
to nothing: the service failed at runtime-init, from the declaration, before
any of its code ran, so it could not degrade — and it could not stop declaring
the group either, since generation requires a declaring service.

The group is now delivered with an empty ROUTES. Nothing decodes it: the
gateway stands its hop only where `hop/MODULES` names modules, and
freezeDeliveredHopAddresses skips an empty value, so DecodeRouting is never
reached. The routing snapshot of an empty value equals that of no group, so no
run is told its routes changed. A composition that names modules still gets its
generated routes, and a workspace no service of which declares the group still
gets nothing.

`codefly doctor workspace` is the same seam, not a second fix: it takes
generated groups from this inventory ("never from a name exemption"), so it now
reports the group as supplied by the invocation instead of
`configuration_missing`. The baselined refusal that asserted the old failure
("no selected modules") is replaced by a test of the new behaviour rather than
kept green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
codefly-dev/authority#42 merged as bfe4876a. The pseudo-version this PR
carried, e2fba9d058e1, exists only on that PR's branch: a squash-merge and
branch delete would orphan it and break `go mod download` here. The library
tree is byte-identical between the two.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The branch-only e2fba9d058e1 is no longer required; only the merged
bfe4876a is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@antoinetoussaint-byte antoinetoussaint-byte changed the title run: merge every composed module's fixture-composition fragment fixture-composition fragments merged by the run, and hop-routes delivered where no module is named Oct 27, 2026
codefly-dev/authority#43 merged as 5178f016, adding the intra-document half of
the one-statement-one-source rule: Decode refuses the same (organisation, role,
kind), installation or stand-in module stated twice within ONE fragment.

The bump is about the diagnostic, not correctness. Without it a duplicate
inside one fragment is still refused — but at the authority's boot-time Decode
of the MERGED document, which names the merged file. The developer is then told
the wrong file is wrong: the generated workspace document rather than the module
fragment that owns the duplicate. With this pin the merge refuses it where it is
written, naming that fragment.

The orphaned bfe4876a pseudo-version is dropped from go.sum: `go get` leaves it
behind, and it names a ref that is no longer required.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit d08ead4 Oct 27, 2026
19 checks passed
crisfiorini-oss pushed a commit to crisfiorini-oss/cli that referenced this pull request Oct 27, 2026
…lies the merge (codefly-dev#1200)

* fix(configurations): fixture-composition is multi-provider by design, and the composition resolves it

Since codefly-dev#1197 every composed module ships a fixture-composition fragment and the
run merges them. Core's single-provider rule then refuses the composition: two
composed modules defining one group differently is ambiguity with no principled
winner — right for every group whose value one module owns, wrong for this one,
where each module is EXPECTED to carry its own part and the parts combine. The
moment a composition composes a second fragment-shipping module,
`codefly doctor workspace` reported

  required workspace configuration "fixture-composition" is ambiguous
  (required by authority/local): provided by composed modules "documents" and
  "authority"; declare it in the solution workspace to resolve the ambiguity

and refused a composition a run handles happily — platform-core#117, moving the
documents pin to v0.2.0, which is the hosted render's last wall. The suggested
remedy is also a refusal now: `codefly generate fixture-composition` will not
overwrite a hand-written document, and the local authority reads the merge, not
one module's part.

The group now has a provider: the shared WorkspaceConfigurationReader supplies
the merge of every composed module's fragment, exactly as it already supplies
hop-routes. Nothing is exempted from the single-provider rule — a group with a
provider is not ambiguous — and because that reader is the one loader a run, CI,
a deployment and the readiness check share, none of them can disagree about this
group. A workspace that answers for itself keeps its answer, so a developer's
generated file and the file a run writes before it loads any configuration both
win as before, through core's own precedence.

`AmbiguousWorkspaceConfigurations` drops every name the reader supplies, and the
doctor does the same for the groups this invocation supplies. Both are keyed on
what was supplied, not on a name, so a second generated group needs no second
exemption.

Tests: two modules shipping fixture-composition is ready and merges, through
the reader and through the readiness check; the workspace's own document still
wins; and an ordinary group two modules disagree on is still ambiguous and still
refused with the same diagnostic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(configurations): merge fixture fragments before accepting module defaults

* test(run): make delivery deadline diagnostics deterministic

---------

Co-authored-by: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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