Skip to content

fixture-composition: one statement stated once inside a fragment, and apply what can fail first - #43

Merged
antoinetoussaint-byte merged 2 commits into
mainfrom
fixture/one-statement-one-source
Oct 27, 2026
Merged

antoinetoussaint-byte merged 2 commits into
mainfrom
fixture/one-statement-one-source

Conversation

@antoinetoussaint-byte

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

Copy link
Copy Markdown
Collaborator

What

The three findings from the review of #42, which is merged. Two are fixes; the third is an accepted limit written down.

1. One statement, stated once — inside a fragment too

The one-source rule was enforced per source, so one fragment could state the same (organization, role, kind) twice and silently union {id: seeded} with {all: true}: the exact widening the rule exists to stop, inside one file, where whoever reads the first statement has no reason to look further down. Decode now refuses a duplicate grant, installation or stand-in module within one document, naming it — which also holds the merged document to the rule, not just its inputs. ErrConflict now reads "one statement, stated twice" and covers both directions.

The grants of a kind are a union, which is why one file stating a kind twice widens it exactly as two files would. A role stating two different kinds, and two roles stating one kind, stay valid.

2. ApplyComposition applied what can fail last

It wrote the role grants, then loaded the installations and stand-in modules — so a document that failed while being applied had already widened the fixture's roles, against that same function's "nothing is applied unless the whole document is valid". The two applying steps are now ordered: LoadFixture (the one that can fail) first, the role writes (which cannot) after. LoadFixture's own internal steps are not individually reversible; the comment says so rather than implying otherwise.

The illustration matters, and my first one was wrong. A refused installation is precisely the case that was already safe: an owner the fixture does not define is caught by checkComposedSetup, pure validation that runs before either applying step in both orderings. My first test used that scenario, so it could not fail — reverting only the ordering left it green, and the whole pkg/business/state suite with it. A guard about a name.

The reachable bug needs a document that gets past validation and fails inside LoadFixture: a stand-in module named after a fixture person. checkComposedSetup only checks its kinds and logs a line for it; the core refuses it when the manifest is applied (principal %q names a person; a module's principal is its own). TestADocumentRefusedWhileApplyingLeavesTheRolesAlone uses that, and I diagnosed it by putting the bug back: against the pre-fix ordering it fails with the editor holding ledger.entry approve from a refused document; with the fix the role is untouched.

3. Ownership is not enforced — an accepted limit, now written down

A statement is keyed by organisation, role and kind alone, never by which module ships the fragment, so a module's fragment can grant at another module's kinds — and a composition's fragment does, by design: the robin playground grants its viewer documents.collection on the collection its own seed writes, a kind the document store owns. "One definition per module" is therefore a convention about where a grant is written, enforced only in that two fragments cannot state the same grant; it is not a check that a module grants on its own kinds.

Both READMEs now say this plainly, with why nothing is lost: these fragments reach the development fixture alone, never a host, and a grant at a kind no composed module serves resolves to nothing (the authority intersects every capability with the delivered manifests), so the failure mode is a refusal, not a widening. Enforcing ownership would need each kind's owning module named in the document, which v1 does not carry — a v2 question, not a silent gap.

Evidence

  • libraries/fixture-composition/go: go vet + go test -race green. New test covers the widening pair, two role entries of one kind, a duplicate installation, a duplicate stand-in — each refused as ErrConflict by Decode and named against its fragment by Merge — and the three shapes that are not duplicates.
  • modules/authority/services/local/code: go vet ./... + go test -race ./... green, with a new regression that fails against the pre-fix ordering (verified by reverting the production change) and passes with it.
  • tools/noproxygate and tools/packagegate green. gofmt clean (it also fixes an import ordering fixture-composition: one fragment per module, merged by the run #42 left in state.go).

A test changed deliberately

TestCompositionRefusesByName prepends a valid entry to every roles case so a refused later entry cannot leave it applied. That entry used the cases' own kind, so under the new rule every case became a duplicate and stopped testing what it names. Its kind moves to ledger.audit, keeping the test's intent; the comment says why. No case was removed and no refusal was weakened.

Fix or hack

Both changes are fixes at the owner. The third is neither — it is a limit named in the docs instead of a check that would need a schema version.

🤖 Generated with Claude Code

antoinetoussaint-byte and others added 2 commits October 27, 2026 12:15
…nt too; and apply what can fail first

Three findings from the review of #42.

One statement, one source, was enforced per SOURCE only: one fragment could
state the same (organisation, role, kind) twice and silently union
`{id: seeded}` with `{all: true}` — the exact widening the rule exists to stop,
inside one file, where whoever reads the first statement has no reason to look
for a second. `Decode` now refuses a duplicate grant, installation or stand-in
module within one document, which also holds the merged document to the rule.

`ApplyComposition` wrote the role grants before loading the installations and
stand-in modules, so a composition whose installation the fixture then refused
had already widened the fixture's roles, against the same function's "nothing
is applied unless the whole document is valid". The two applying steps are now
ordered: the one that can fail (LoadFixture) runs first, the one that cannot
(the role writes) after.

The third is an accepted limit, written down rather than fixed: a statement is
keyed by organisation, role and kind alone, so a module's fragment CAN grant at
another module's kinds — and a composition's fragment does, by design (the
robin playground grants its viewer the collection its own seed writes). "One
definition per module" is a convention about where a grant is written, not
ownership enforcement; both READMEs now say so, with why nothing is lost by it
(development fixture only, and a grant at an unserved kind resolves to nothing).

The refusal table's prepended valid entry moves to its own kind: one document
states each (organisation, role, kind) once, so reusing the cases' kind would
have made every case a duplicate and hidden what it tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ails while APPLYING

TestARefusedInstallationLeavesTheRolesAlone could not fail: the refusal it
provoked ("not a person of acme") comes from checkComposedSetup, pure
validation that runs before either applying step in BOTH orderings, so the
scenario never reached the code the fix moves. Reverting only the ordering left
it green, and the whole pkg/business/state suite with it — a guard about a name.

Replaced by TestADocumentRefusedWhileApplyingLeavesTheRolesAlone, whose
document gets past validation and fails INSIDE LoadFixture: a stand-in module
named after a fixture person, which checkComposedSetup only checks the kinds of
and logs a line for, and which the core refuses when the manifest is applied
("names a person; a module's principal is its own").

Diagnosed by putting the bug back: against the pre-fix ordering the new test
fails with the editor holding `ledger.entry approve` from a document that was
refused; with the fix the role is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte merged commit 5178f01 into main Oct 27, 2026
17 checks passed
antoinetoussaint-byte added a commit to codefly-dev/cli that referenced this pull request 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>
crisfiorini-oss pushed a commit to crisfiorini-oss/cli that referenced this pull request Oct 27, 2026
…ered where no module is named (codefly-dev#1197)

* feat(run): merge every composed module's fixture-composition fragment 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>

* fix(generate): fixture-composition materializes pinned modules first, as a run does

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

* fix(fixture-composition): the lint gate's four findings on the new code

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>

* fix(orchestration): a service declaring hop-routes with no module named 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>

* chore: drop the go.sum entries a local golangci-lint install added

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

* build: pin the fixture-composition library to its merged commit

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>

* build: drop the orphaned fixture-composition pseudo-version from go.sum

The branch-only e2fba9d058e1 is no longer required; only the merged
bfe4876a is.

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

* build: pin the fixture-composition library past authority#43

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>

---------

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