Skip to content

docs: chapter 40, composition - #62

Closed
ExtraToast wants to merge 1 commit into
docs/ch30-deliverablesfrom
docs/ch40-composition
Closed

docs: chapter 40, composition#62
ExtraToast wants to merge 1 commit into
docs/ch30-deliverablesfrom
docs/ch40-composition

Conversation

@ExtraToast

Copy link
Copy Markdown
Contributor

Chapter 40. Stacked on #61#60#59#58; each base retargets as the one below merges.

Composition turns many independently-published declarations into the single global view layer 2 needs. Seven properties cannot be evaluated against one repository — Service Id uniqueness, hostname label uniqueness, the reconcile DAG, the inbound derivations, the reader set of a secret path, reachability completeness, and co-test membership. No Service knows its own consumers; that is the whole argument.

Resolves open item 5, and refines ADR-0015

The unit of publication is a repository, not a domain, and a fragment declares which domains it contributes to. So homelab-collections may stay one repository publishing one fragment for five domains, or split into five — composition behaves identically. The split becomes a convenience rather than a prerequisite, and no decision is needed.

The justification for "the lock is an output" was in the tree

I'd argued this from first principles before. The actual evidence is better: an artefact cannot contain its own digest.

homelab-inventory's published context/public/context-manifest.yml ships with packageDigest: "", and the workflow calls oras resolve after oras push to learn the digest. A design where each repository pinned its peers would require every fragment to know digests that cannot exist at authoring time.

That repository's two-hash discipline is copied for the same reason it works there:

hash over why
sourceSha the git commit provenance
inputsSha authored inputs only, never generated outputs change detection that doesn't chase its own tail

inventorySourceSha is derived from inventory/ + catalog/ + vault/ + providers/ and never from the generated context/ tree — and is identical in both published manifests. A hash including generated output would change on every publish and detect nothing.

Order-independence forces a design consequence

Composition must yield the same result regardless of pull order. That is not a nicety — it means every merge must be commutative, so every collision is an error rather than a last-write-wins merge. There is no precedence between fragments, and no fragment can override another.

The normative content: 22 invariants

Four groups — identity, references, secrets, completeness — each with an error code. Composition fails on any one and produces no ComposedIntent, so nothing renders.

Two worth calling out:

  • Optional edges are excluded from the cycle check deliberately. required: false means a Workload starts without its peer, so a cycle through optional edges cannot deadlock a rollout.
  • E_ROLL_AFFECTS_OTHER_READERS is the check nothing in the estate has today, and the motivating case is live: secret/platform/observability holds the Prometheus token, the Discord webhook and the Grafana client secret, and one CronJob rolls one of those keys.

And E_PARTICIPANT_MISSING is not pedantry. Flux prunes — a domain that fails to publish is not merely absent from the render, it is deleted from the cluster by a render that looks entirely valid.

The lock

Generalises cluster-composition-lock from two pinned contexts to N pinned fragments, inheriting lockChain so "when did this fragment's digest change, and which render did that produce" is answerable without diffing published artefacts.

Four open items, one of which matters soonest

"On release" is underspecified. A service repository releases when its image releases, so a change to service.yml alone may not cut a release — meaning an intent change could sit unpublished behind a maxAge window. Either intent publishes on merge independently of the image release, or maxAge is doing work it should not.

The others: who runs composition and how often; whether the union may span clusters (the lock is cluster-keyed but Service Id uniqueness is estate-wide); and fragment signing, since composition verifies MANIFEST.sha256 per file but not provenance while deploy-artifact.yml already carries id-token: write and attestations: write.

Composition turns many independently-published declarations into the single
global view layer 2 needs. Seven properties in this specification cannot be
evaluated against one repository: Service Id uniqueness, hostname label
uniqueness, the reconcile DAG, the inbound derivations, the reader set of a
secret path, reachability completeness, and co-test membership. No service knows
its own consumers, which is the whole argument.

REFINES ADR-0015 and resolves open item 5. The unit of publication is a
repository, not a domain, and a fragment declares which domains it contributes
to. So homelab-collections may stay one repository publishing one fragment for
five domains, or split into five publishing one each -- composition behaves
identically. The split becomes a convenience rather than a prerequisite, and no
decision is needed.

Found the concrete justification for making the lock an output, in the tree
rather than by argument: an artefact cannot contain its own digest.
homelab-inventory's published context-manifest.yml ships with packageDigest: ""
because the digest does not exist until the push completes, and the workflow
calls oras resolve after oras push to learn it. A design where each repository
pinned its peers would require every fragment to know digests unknowable at
authoring time.

Also copies that repository's two-hash discipline. sourceSha is the git commit,
for provenance. inputsSha covers authored inputs only and never generated
outputs -- homelab-inventory derives inventorySourceSha from inventory, catalog,
vault and providers and never from the generated context tree, and the value is
identical in both published manifests. A hash including generated output would
change on every publish and detect nothing.

Composition is order-independent, and that forces a design consequence rather
than being a nicety: every merge must be commutative, so every collision is an
error rather than a last-write-wins merge. There is no precedence between
fragments and no fragment can override another.

The normative content is 22 estate-wide invariants across four groups --
identity, references, secrets, completeness -- each with an error code.
Composition fails on any and produces no ComposedIntent, so nothing renders.

Two are worth calling out. Optional edges are excluded from the cycle check
deliberately: required: false means a workload starts without its peer, so a
cycle through optional edges cannot deadlock a rollout. And
E_ROLL_AFFECTS_OTHER_READERS is the check nothing in the estate has today, with
a live case motivating it -- secret/platform/observability holds the Prometheus
token, the Discord webhook and the Grafana client secret, and one CronJob rolls
one of those keys.

E_PARTICIPANT_MISSING is not pedantry. Flux prunes, so a domain that fails to
publish is not merely absent from the render, it is deleted from the cluster by
a render that looks entirely valid.

The composition lock generalises cluster-composition-lock from two pinned
contexts to N pinned fragments, inheriting lockChain so "when did this
fragment's digest change, and which render did that produce" is answerable
without diffing published artefacts.

Four open items, the first of which matters soonest: "on release" is
underspecified, because a service repository releases when its image does, so an
intent-only change could sit unpublished behind a staleness window.
@ExtraToast ExtraToast added type: docs Documentation-only change. area: deploy homelab-deploy, deploy-v2, manifests, and rollout flow. component: config Configuration schema, defaults, or repo settings. labels Aug 31, 2026
@ExtraToast

Copy link
Copy Markdown
Contributor Author

Superseded by #64, which landed chapters 20-60 together. The stack could not be rebased after #58 and #59 squash-merged: each branch was cut from the pre-squash tip of the one below. Kept open until now as the per-chapter review record.

@ExtraToast ExtraToast closed this Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: deploy homelab-deploy, deploy-v2, manifests, and rollout flow. component: config Configuration schema, defaults, or repo settings. type: docs Documentation-only change.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant