design — the single statement of the boundary. Index:
../../INDEX.mdStatus: the front door for the target boundary. Captured 2026-07-11. This page combines shipped behaviour with the design that must precede the rest of the boundary. Rows marked planned state the intended contract, not implemented behaviour; other rows describe the current runtime. If a claim here and a claim there disagree, this page is wrong and should be fixed — the boundary has one home now.
GitOps Reverser should edit the intent layer. It must never edit the expansion layer.
Every GitOps repository has an intent layer — the documents a human authored and committed — and an expansion layer — the objects a controller derived from them. We mirror and edit the intent layer, because that is the layer with a home in Git and the layer a careful human would edit anyway. The target boundary refuses the expansion layer, because expansion is a one-way function and its output has nowhere to go.
That refusal is the operator's defensible support contract, not a gap — but the live-object provenance gate that enforces it is not implemented yet. In particular, the runtime currently does not distinguish controller-expanded objects before sanitization. See expansion-boundary-and-corpus-organisation.md.
An edit must survive all three. They are independent, and each has its own design doc.
| # | Question | Failure if we get it wrong | Where it is argued |
|---|---|---|---|
| 1 | Is it KRM, and may I read/write it? | we corrupt a document we did not understand | resource-capability-model.md |
| 2 | Can this edit be written back to exactly one document? | we cannot express the change, or we change more than one thing | kustomize-support-boundary.md, gittarget-granularity-and-cross-environment-edits.md §1 |
| 3 | Does that document already have another writer — and does this object even have a home in Git? | we destroy someone else's invariant, or invent a second source of truth | orchestrator-knowledge-boundary.md, expansion-boundary-and-corpus-organisation.md |
The governing rule of question 2 is fan-in = 1: we never change more than one thing. An edit may be written to a document only if exactly one live object derives from it. That single rule refuses shared kustomize bases, ApplicationSet templates, and multi-input ResourceSet templates alike. It is specified once, in gittarget-granularity-and-cross-environment-edits.md §1 (the two-layer write boundary, L1/L2); everywhere else it is quoted, not redefined.
| Verdict | Meaning |
|---|---|
| Editable | We mirror it and write edits back to its file. |
| Read-only context | We read it to understand the folder; we never write it. |
| Not mirrored | It exists live, but a controller synthesised it and Git has no home for it. This needs the planned provenance gate. |
| Refused | The folder cannot be written safely. We say why, and fail the target. |
| Write-only | We can describe and replace it, but never read it (SOPS). |
| Construct | Layer | Verdict | Why |
|---|---|---|---|
| Plain KRM folder | intent | Editable | the files are the objects — the first-class target |
Argo CD Application (hand-written) |
intent | Editable | ordinary KRM |
Argo CD Application generated by an ApplicationSet |
expansion | Planned: not mirrored | carries an ownerReference; no home file |
Argo CD ApplicationSet (the CR) |
intent | Editable as a document | but its template has fan-in N — see below |
App-of-apps root Application |
intent | Editable, flagged cluster entry point | not an onboarding answer |
Flux Kustomization, GitRepository, OCIRepository, HelmRepository |
intent | Editable | ordinary KRM |
Flux HelmRelease |
intent | Editable | shipped; an ordinary KRM document like any other |
| Objects the helm-controller renders from it | expansion | Planned: not mirrored | no home file |
flux-operator ResourceSet (the CR) |
intent | Editable as a document | spec.inputs is the supported edit surface |
Objects a ResourceSet expands |
expansion | Planned: not mirrored | spec.resources × inputs; no home file |
flux-operator ResourceSetInputProvider |
intent | Editable | but status.exportedInputs is live-only and unwritable |
flux-operator FluxInstance |
intent | Editable, flagged cluster entry point | it manages Flux itself |
KRO ResourceGraphDefinition, Crossplane Composition + claims |
intent | Editable | ordinary KRM |
| Objects they expand | expansion | Planned: not mirrored | no home file |
| Construct | Verdict | Why |
|---|---|---|
kustomize resources, namespace, images, replicas |
Editable | invertible; images/replicas edit-through is shipped |
| kustomize base + un-fancy overlays | Editable, narrow | render-root scoping reads ../../base as read-only context and writes only inside the overlay; the base is never written. It edits existing overlay-local documents and declared image/replica entries, and authors a new images:/replicas: entry when a base-supplied image/replica is changed in one environment. scan-repo still reports these as overlay-fan-out-unsupported — the discovery-side flip is a follow-up |
| New object in an external-base overlay | Editable | placed as an overlay-local file and registered in the overlay's own resources: entry (never the base's), verified by re-render before the commit; proven by TestPlacement_ExternalBaseOverlay_NewObject. A new object a folder images:/replicas: entry would override is refused loudly by the oracle, not committed |
| Change a base-supplied image/replica in one overlay | Editable | the writer authors a new images:/replicas: entry in the overlay's own kustomization (the base stays read-only), verified by re-render; proven by TestOverlayAuthors_ImageEntry_ForBaseSuppliedImage / ..._ReplicaEntry_ForBaseSuppliedCount |
| Delete a base-inherited object in one overlay | Editable | the writer authors a $patch: delete (patch file plus patches: entry) in the overlay, verified by re-render that the object leaves the render; the base is untouched. Proven by TestOverlayAuthors_DeletePatch_ForInheritedObject. A patch that fails to match is refused by the oracle, not committed |
| kustomize base shared by >1 overlay, edited in place | Refused | fan-in > 1 |
kustomize patches: — a strategic-merge document named by path: |
Tolerated, not authored | the folder is accepted and the render is mirrored; the patch is read-only build context. An edit to a field the patch OWNS is refused per object, not per folder |
kustomize patches: — inline, JSON6902, or a path outside the tree |
Refused | not a sparse KRM document we can read; refused by name |
kustomize patchesStrategicMerge, patchesJson6902 (deprecated spellings) |
Refused | kustomize does not fold them into patches: (measured), so they refuse under their own names |
kustomize generators, components, namePrefix/nameSuffix, remote bases |
Refused | non-invertible |
kustomize helmCharts: (inflation) |
Refused | we never render a chart |
A Helm chart (Chart.yaml + templates/ + values.yaml + crds/) |
Planned: skipped as a unit | this needs chart-folder detection; today scan-repo can report an incidental crds/ directory as accepted |
Helm knobs on a HelmRelease / Application (chart version, inline values, parameters) |
Editable | the Helm surface people actually use — see below |
Free-standing Helm values file (values/production.yaml) |
Refused today | not KRM; a ValuesFile projection is proposed |
CI-rendered rendered/ output |
Refused | the next render destroys the edit |
| Construct | Verdict | Why |
|---|---|---|
A path written by Flux ImageUpdateAutomation |
Refused | a bot commits here; $imagepolicy comments are load-bearing |
.argocd-source*.yaml (Argo CD Image Updater) |
Refused / read-only | written by a bot, and it outranks the config a reader would inspect |
SOPS Secret |
Write-only | not schema-conformant; the mac binds the whole document |
SealedSecret |
Editable | ordinary KRM; each value sealed independently |
ExternalSecret |
Editable | ordinary KRM; the value simply lives elsewhere |
Secret derived by ESO / sealed-secrets |
expansion → Planned: not mirrored | carries an ownerReference; the mirror-path gate is unbuilt |
We already do more Helm than that sentence admits, and the honest headline is:
We support Helm the way GitOps repositories actually use it — as a declaration you edit, not a chart you render.
The most common Helm operations in a GitOps repo are bump the chart version and
change a value on a HelmRelease, and both have worked since higher-level KRM
documents landed. What stays
refused, permanently and correctly, is chart inflation: we never render
templates/, and we never learn what a value means.
The one genuine gap is the free-standing values file, which is not KRM and so
has no object for a user to edit. The proposed fix reuses the
EncryptedSecret trick — project it into a kind
we can store — and is argued in
expansion-boundary-and-corpus-organisation.md.
- No orchestrator emulation. We do not resolve a Helm chart, evaluate an
ApplicationSet generator or a ResourceSet template, decrypt SOPS, or contact a
registry. We do run kustomize — the real
sigs.k8s.io/kustomize/api, the library Flux itself renders with — locally, with plugins disabled, and never on a remote base: a remote resource is refused before the build is invoked. Using the renderer is the opposite of emulating it; see kustomize-support-boundary.md §4. - No Argo CD or Flux Go dependency. Their kinds are matched by group and kind
over
unstructured; the upstream checkouts underexternal-sources/are reference material, never vendored code. (sigs.k8s.io/kustomizeis neither — it is a Kubernetes SIG library, already in this module's requirement graph.) - We never widen the boundary by guessing. Ownership and provenance only ever refuse more. Everything refused for renderability stays refused.
| Doc | What it settles |
|---|---|
../../facts/expansion-provenance-markers.md |
the measured evidence each producer leaves behind — and why the ownerReference gate catches one producer in five |
| README.md | the index of this folder, and the summary of what works today |
| kustomize-support-boundary.md | the kustomization field taxonomy and the supported-layout allowlist |
| gittarget-granularity-and-cross-environment-edits.md | the write boundary (L1/L2) — the one home of fan-in = 1 |
| orchestrator-knowledge-boundary.md | where Flux/Argo knowledge lives: claims about paths, not a dependency |
| expansion-boundary-and-corpus-organisation.md | the provenance axis; ApplicationSet vs ResourceSet; the Helm surface |
| resource-capability-model.md | what may I do to this document (schema conformance, visibility, write unit) |
| write-only-encrypted-secrets.md | SOPS: describe the ciphertext, never read it |
| sealed-secrets-and-external-secrets.md | why neither needs a new kind, and the derived-object gate |
| unreflectable-edits-and-write-gating.md | what happens to an edit with no legal destination |
| repo-discovery-and-onboarding-scan.md | reporting all of this over a whole repository |
test/fixtures/gitops-layouts/ |
the corpus of real-world shapes every verdict is checked against |