Skip to content

fix(kustomize): stop writing the build's own output into the build's input#234

Merged
sunib merged 13 commits into
mainfrom
fix/kustomize-source-form-projection
Jul 15, 2026
Merged

fix(kustomize): stop writing the build's own output into the build's input#234
sunib merged 13 commits into
mainfrom
fix/kustomize-source-form-projection

Conversation

@sunib

@sunib sunib commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

The bug, measured

The writer mirrors a live object into the file that produced it. Under kustomize that file is not what the cluster runs — the build stands between them — so mirroring the live object straight back writes the build's own output into the build's input.

This is not hypothetical, and it is not about patches. It is live today, in a folder we accept today:

# kustomization.yaml (supported: labels + commonAnnotations, no patch, no generator)
labels:
  - pairs: {env: prod}
commonAnnotations: {owner: platform}
# deployment.yaml — after one reconcile of an UNCHANGED, in-sync folder
metadata:
  labels: {env: prod}          # <- the OVERLAY's, absorbed into the BASE
  annotations: {owner: platform}

Nothing changed in the cluster. Nothing changed in the render. Every reconcile of an in-sync folder produced a commit, and the file was left wrong — delete the kustomization later and the injected values are now yours forever. In a base shared by two overlays, the value baked in is one environment's.

render-root-scoping.md §2 predicted exactly this ("the metadata-transformer leak, live today, in supported folders"). Nobody had run it.

The fix

One rule, and it models no transformer:

Where the live object and the render agree, the source keeps its bytes.
Where they disagree, the user changed something, and that is what we write.

Agreement means the build already produces exactly what the cluster runs, so the source is — by construction — the thing that produced it. Keeping it is not a heuristic; changing it would be writing a value we did not get from the user. Disagreement is the user's edit: routed to an images:/replicas: entry when the dye says one supplies the field, written through to the source otherwise — where, if the build owns the field, the re-render refuses the flush.

Because it needs no model of commonLabels, of namespace, or of a patch, it closes all of them at once.

Why this is the gate on patches:

I started on the patches: milestone and found this in front of it. A patched base absorbs the overlay's values in exactly the same way — and no re-render can catch it: the patch re-imposes its own value, so the render comes out identical either way and the oracle passes. VerifyBatchRenders guards the render; only this guards the source.

Tolerating patches: lands next, on top of this.

Behavior changes

  • The re-render now runs for any document a render root produces, not only one an override chain governs. A change to a build-supplied field in a folder with no images:/replicas: entries used to be committed and silently never converge (the transformer overrode it right back, forever). It is now a reported refusal — WriteBoundaryRefused, naming the file and the object.
  • A live change the projection cannot place is refused (unplaceable-edit): the build and the user have both rewritten one list whose elements carry no unique name: to pair them by. Pairing by position is not a conservative guess, it is measurably wrong — kustomize prepends a container a patch adds, so the source's element 0 is the render's element 1.

Both are in docs/UPGRADING.md.

Measured, not reasoned

Every fact here came from asking kustomize with a throwaway probe, per the workstream's own rule:

Probe Answer
labels:/commonAnnotations: on an in-sync folder the injected metadata is committed into the source manifest
a patch on an in-sync folder the patched CPU request is committed into the base
an SMP that adds a container kustomize prepends it — index alignment is wrong, not merely fragile
patch vs. images:/replicas: ordering [PatchTransformer, ReplicaCountTransformer, ImageTagTransformer] — the transformers win, so a patch that sets a governed field is dead text

The last one is now a permanent fixture (TestRender_ATransformerOverridesAPatchOnTheSameField) — it is the fact a future refactor will break.

The net

TestProjection_InSyncCorpusFolderIsANoOp now compares the whole document, not just its image slots — which is how a projection that quietly rewrote every field we had not modelled passed it for as long as it did. It went from 8 documents checked to 53, across every fixture in both corpora.

Line delta, honestly

+430 source (source_form.go is 250 of it), +410 test. It is not a small change for a projection that used to be "copy the live object". But the thing it replaces was not simpler, it was just wrong in a way that only showed up in files nobody diffed.

Validation

task lint · task test (coverage 75.6% → 75.7%) · task test-e2e (55/55) all pass.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Prevented build-injected labels, commonLabels, commonAnnotations, and namespace from being written back into source when they already match the live object.
    • Strengthened flush/projection safety by refusing writes when rendered content can’t be faithfully represented in source, including render-vs-live ${...} token mismatches and ambiguous list placement.
  • New Features
    • Added the RenderMatchesLive GitTarget condition (including kubectl table column) to reflect render-vs-live fidelity and block normal writes while not established.
  • Documentation
    • Updated upgrade guidance with the new breaking-change behavior and any required cleanup for previously injected metadata.

…input

The writer mirrors a live object into the file that produced it. Under kustomize
that file is not what the cluster runs, and mirroring the live object straight
back writes the BUILD'S OUTPUT into the build's INPUT.

Measured, on a folder we accept today, with nothing changed in the cluster and
nothing changed in the render: a kustomization declaring `labels:` +
`commonAnnotations:` and nothing else commits the overlay's `env: prod` and
`owner: platform` into the base manifest, on the first reconcile of an unchanged
folder. Every reconcile of an in-sync folder produced a commit, and the file was
left wrong. In a base shared by two overlays, the value baked in is one
environment's.

The fix is one rule, and it models no transformer:

    WHERE THE LIVE OBJECT AND THE RENDER AGREE, THE SOURCE KEEPS ITS BYTES.
    WHERE THEY DISAGREE, THE USER CHANGED SOMETHING, AND THAT IS WHAT WE WRITE.

Agreement means the build already produces exactly what the cluster runs, so the
source is — by construction — what produced it, and there is nothing to write.
Disagreement is the user's edit: it is routed to an images:/replicas: entry when
the dye says one supplies the field, and written through to the source otherwise
— where, if the build owns the field, the re-render refuses the flush.

Because it needs no model of labels, of namespace, or of a patch, it closes all
of them at once. It is also the gate on tolerating `patches:` at all: a patched
base would otherwise absorb one environment's values, and no re-render can catch
that (the patch re-imposes its value, so the render comes out identical).

Two behavior changes go with it:

  - the re-render now runs for ANY document a render root produces, not only one
    an override chain governs. A change to a build-supplied field in a folder
    with no images:/replicas: entries used to be committed and silently never
    converge; it is now a reported refusal.
  - a live change the projection cannot place is refused (`unplaceable-edit`):
    the build and the user both rewrote one list whose elements carry no unique
    `name:` to pair them by. Pairing by position is not a conservative guess, it
    is measurably wrong — kustomize PREPENDS a container a patch adds.

The corpus no-op invariant now compares the WHOLE document, not just its images,
which is how a projection that quietly rewrote every field we had not modelled
passed it for as long as it did: 8 documents checked before, 53 now.
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3d30b0f5-0393-4a83-b20d-a967310d8c90

📥 Commits

Reviewing files that changed from the base of the PR and between 039e792 and d618b07.

📒 Files selected for processing (13)
  • docs/design/support-boundary/README.md
  • docs/design/support-boundary/kustomize-token-writeback-explained.md
  • docs/design/support-boundary/next-prompt-render-matches-live-gate.md
  • docs/design/support-boundary/orchestrator-reconcile-trigger.md
  • docs/design/support-boundary/render-fidelity-scenarios.md
  • docs/design/support-boundary/render-fidelity.md
  • docs/design/support-boundary/renderer-abstraction-idea.md
  • docs/facts/kustomize-never-emits-dollar-brace.md
  • internal/watch/target_watch.go
  • internal/watch/target_watch_test.go
  • test/e2e/fixtures/render-fidelity/deployment.yaml
  • test/e2e/fixtures/render-fidelity/kustomization.yaml
  • test/e2e/render_fidelity_e2e_test.go

📝 Walkthrough

Walkthrough

The PR adds source-form reconciliation for Kustomize output, render-versus-live token detection, epoch-based write gating, GitTarget status reporting, refusal classification, tests, design documentation, and related CRD and coverage updates.

Changes

Source-form reconciliation and render-fidelity gating

Layer / File(s) Summary
Source-form restoration and projection
internal/manifestanalyzer/source_form.go, internal/manifestanalyzer/overrides_*.go, internal/manifestanalyzer/source_form_test.go
Three-way reconciliation preserves build-supplied fields, routes supported image and replica edits, and refuses ambiguous list placement.
Render-fidelity detection and write enforcement
internal/manifestanalyzer/render_fidelity.go, internal/git/plan_flush.go, internal/git/render_fidelity_gate.go, internal/watch/*
Rendered ${...} tokens are compared with live values; divergences refuse flushes and close normal writes until a complete clean epoch.
Status, validation, and documentation
internal/controller/*, api/v1alpha3/gittarget_types.go, config/crd/bases/*, internal/*/*test.go, test/e2e/*, docs/*, .coverage-baseline
GitTarget conditions and printer output expose fidelity state, while unit, fixture, end-to-end, upgrade, design, and coverage updates document and validate the behavior.

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LiveObject
  participant EventRouter
  participant FidelityGate
  participant BranchWorker
  participant GitTarget
  LiveObject->>EventRouter: resync or live update
  EventRouter->>FidelityGate: record clean or divergent scope result
  FidelityGate-->>BranchWorker: allow or block normal Git writes
  FidelityGate-->>GitTarget: publish RenderMatchesLive status
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is substantive, but it does not follow the repository template and omits required sections like Type of Change, Checklist, and Related Issues. Reformat the PR description to match the template and add the missing sections, especially Type of Change, Testing, Checklist, Related Issues, and Additional Notes.
Docstring Coverage ⚠️ Warning Docstring coverage is 54.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: preventing Kustomize build output from being written back into source manifests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/kustomize-source-form-projection

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/design/support-boundary/render-root-scoping.md`:
- Around line 90-93: Update the `vars` support statement in the support-boundary
document to remove the contradiction: do not claim folders containing `vars` are
accepted today, and describe that behavior as historical or currently rejected
consistent with the parenthetical about `#229`.

In `@internal/manifestanalyzer/acceptance.go`:
- Around line 137-148: Fix the doc comment for IssueUnplaceableEdit by replacing
the malformed sentence with clear, grammatical wording that explains the source
and rendered list elements cannot be paired because they have no unique names.
Remove the stray colon and ensure the SourceFormRefusedError reference is
integrated naturally.

In `@internal/manifestanalyzer/source_form.go`:
- Around line 69-72: Complete the truncated error message returned by
SourceFormRefusedError.Error so it forms a grammatically complete refusal
explanation, preserving the existing context about the changed build/live
objects and unnamed elements. Ensure the full message is surfaced through
WriteBoundaryRefused without altering unrelated error handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e2b06bc3-7e98-4461-9c2e-be479a15b3e7

📥 Commits

Reviewing files that changed from the base of the PR and between 64bebc0 and cb52247.

📒 Files selected for processing (16)
  • .coverage-baseline
  • docs/UPGRADING.md
  • docs/design/support-boundary/render-root-scoping.md
  • internal/git/plan_flush.go
  • internal/git/source_form_test.go
  • internal/manifestanalyzer/acceptance.go
  • internal/manifestanalyzer/analyzer_test.go
  • internal/manifestanalyzer/kustomization_parse.go
  • internal/manifestanalyzer/kustomize_render_semantics_test.go
  • internal/manifestanalyzer/kustomize_render_test.go
  • internal/manifestanalyzer/overrides_attribution.go
  • internal/manifestanalyzer/overrides_projection.go
  • internal/manifestanalyzer/overrides_projection_test.go
  • internal/manifestanalyzer/source_form.go
  • internal/manifestanalyzer/source_form_test.go
  • internal/watch/event_router.go

Comment thread docs/design/support-boundary/render-root-scoping.md Outdated
Comment thread internal/manifestanalyzer/acceptance.go
Comment thread internal/manifestanalyzer/source_form.go
sunib added a commit that referenced this pull request Jul 15, 2026
…e, fix the vars contradiction

The SourceFormRefusedError message (surfaced to the user via WriteBoundaryRefused)
and its IssueUnplaceableEdit doc comment both cut off mid-clause; complete them.
And §2's vars bullet claimed the folder is accepted today while a parenthetical
said it now refuses -- rewrite the passage in past tense (both leaks it cites are
now closed, by #229 and by sourceForm).
…e, fix the vars contradiction

The SourceFormRefusedError message (surfaced to the user via WriteBoundaryRefused)
and its IssueUnplaceableEdit doc comment both cut off mid-clause; complete them.
And §2's vars bullet claimed the folder is accepted today while a parenthetical
said it now refuses -- rewrite the passage in past tense (both leaks it cites are
now closed, by #229 and by sourceForm).
@sunib

sunib commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Review concern: we render kustomize, but not with the orchestrator's context

This PR closes the leak where our render's output leaked into the source. But there is a broader, pre-existing version of the same blind spot it does not close, and we should stop implying it does: the folder we render is not the whole render spec. The orchestrator supplies render context that is not in the repo:

  • Argo CD carries an entire second kustomize layer in the Application object, not the repo: spec.source.kustomize sets images, replicas, patches, components, nameSuffix, commonLabels, namespace, and even the kustomize versionexternal-sources/argo-cd/pkg/apis/application/v1alpha1/types.go:723. None of it is visible by reading the folder.
  • Flux applies postBuild.substitute / substituteFrom after the build, replacing ${var} from cluster ConfigMaps/Secrets — external-sources/flux2/internal/build/build.go:621 (kustomize.SubstituteVariables), plus targetNamespace and object-level patches/images.

So applied = f(repo, orchestrator-context, their-kustomize), while we compute g(repo, our-defaults). Where they differ, sourceForm reads live ≠ our-render as "the user changed it" and writes the orchestrator's value into the source — e.g. Flux substitutes ${REGION}us-east, our render keeps ${REGION}, and we bake us-east into the file, destroying the template. And the oracle does not catch it, because VerifyBatchRenders re-renders with our kustomize too — it shares the blind spot (the render-attribution §5 "verification must not share the blind spot of the thing it verifies" failure, one level up: our whole renderer shares the orchestrator's blind spot).

The docs frame the residual as version skew only (render-root-scoping.md §3, render-attribution.md §8). That under-states it: version is the least likely axis to bite; the Argo overrides and Flux postBuild are the likely ones.

Proposed direction — support a little too little, not a little too much

  1. Narrow the claim + a free in-repo refusal. State the fidelity precondition in support-contract.md: edit-through is sound only when our render equals the orchestrator's — i.e. no out-of-band context. And refuse edit-through on any object carrying ${...}/$(...) tokens (almost always Flux postBuild/vars — our render leaves them literal). No orchestrator awareness needed.
  2. A reality-check precondition (the general net, and our own method). We already hold both the live object and our render. For every object the flush does not claim (the oracle's byte-identical set), our render should already equal live; if it does not, our model disagrees with reality — unmodeled context or drift — so refuse this flush. This turns unknown-unknown context into a measured signal, exactly as the dye turns "what does kustomize do" into a measurement instead of an assertion.
  3. Orchestrator awareness (gated on the interpreter model in orchestrator-knowledge-boundary.md). Read the Flux/Argo object, detect the render-affecting fields, and name the cause in the refusal (TransformedOutOfBand in that doc's claim vocabulary) — eventually modelling the safe ones (match the pinned version).

None of this blocks this PR — the sourceForm fix is strictly better and correct on its own. It is about not marketing "kustomize edit-through" on the back of it without the precondition.

sunib and others added 2 commits July 15, 2026 07:59
…lementation

Bring the support-boundary design docs onto this PR — they are the design that sits
next to this real implementation, so they belong together rather than in a separate
PR: render-fidelity.md (our render is not the orchestrator's; the render-vs-live
fence, the blocking RenderFaithful condition, and where to implement it),
admission-consent.md, orchestrator-reconcile-trigger.md, and the kpt-and-krm-functions
orientation note, plus their README index rows. Consolidated from PR #237, now closed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…atchesLive

RenderMatchesLive names what it measures directly — does our render match the live
object — better than the 'faithful' framing. The negative reason follows:
RenderNotFaithful -> RenderDoesNotMatchLive, and the gate AcceptRenderFaithful ->
AcceptRenderMatchesLive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/design/support-boundary/admission-consent.md`:
- Around line 120-125: Update the consent token design around the consequence
content hash to hash a canonical representation of the complete consequence,
including affected fields, old and new values, and source revision—not only
object/environment pairs. Include the relevant GitTarget and authorization scope
in the hashed input, and require authorization to be rechecked at flush time
before honoring consent.

In `@docs/design/support-boundary/render-fidelity.md`:
- Around line 224-234: The document overstates what RenderMatchesLive proves:
the described token-only check cannot establish full render fidelity or safely
serve as an adoption gate. Either rename and narrow the condition to token
fidelity throughout the affected sections, or implement the general
untouched-field precondition so Argo overrides, post-build changes, and version
skew are covered before retaining the full-fidelity claim.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2a64f97c-7f80-440a-9771-c3bf1f7ae722

📥 Commits

Reviewing files that changed from the base of the PR and between a841de8 and 9bf0425.

📒 Files selected for processing (5)
  • docs/design/support-boundary/README.md
  • docs/design/support-boundary/admission-consent.md
  • docs/design/support-boundary/kpt-and-krm-functions.md
  • docs/design/support-boundary/orchestrator-reconcile-trigger.md
  • docs/design/support-boundary/render-fidelity.md

Comment thread docs/design/support-boundary/admission-consent.md
Comment thread docs/design/support-boundary/render-fidelity.md Outdated
sunib and others added 5 commits July 15, 2026 08:30
…tation prompt

Align the prose to the RenderMatchesLive framing (the "faithful/unfaithful"
language follows the rename to "matches / diverges"), and mark §8's 5a-first as
decided rather than open. Add next-prompt-render-matches-live-gate.md: a handoff
for a fresh session to implement the token gate — the design, the code entry points
(patchExisting + the resync path), the CRD lesson (measure render-vs-live, not the
disk; the reverted structural check broke CRD mirroring), the test net, and the
e2e-tail gotcha.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Incorporate the review findings:

- The predicate must compare the RENDER to live, not the Git source to live.
  kustomize does not RESOLVE ${...}, but it does not PRESERVE every token-bearing
  source field either: a supported labels/commonLabels transform overwrites
  metadata.labels[...] via SetEntry, so `env: ${ENV}` under `labels: {env: prod}`
  renders to `prod` == live. Git-vs-live would falsely refuse that faithful folder.
  The render is dm.Rendered.Object for a kustomize doc, the Git doc for a plain one,
  and reading the render also catches a token a patches: block injects. Fixed the
  prompt (which had the git-vs-live shortcut + "does not need dm.Rendered") and §5a.

- Narrow the causal claim in the fact doc and the design: a rendered token that
  differs from live proves only that our render did not produce the value — could be
  Flux substitution, a live edit, admission, another controller. Refusal is safe
  regardless; RenderDoesNotMatchLive is a fact, "must have been substituted" a guess.

- State the two integration requirements for the blocking gate: aggregate fidelity
  across ALL scoped resyncs (a last-successful-GVR status masks a diverging type),
  and stop opening write windows while failed (status-only refusal isn't a gate).

- Record the bias: blocking a shade too soon is fine; failing to block is not. The
  simple token gate is the right first cut.

Also fixes the fact doc's external-sources markdown links (doccheck treats upstream
checkouts as untracked) to code spans, matching the convention in the sibling docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
internal/controller/gittarget_controller.go (1)

205-207: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use git.RenderFidelityState constants instead of string literals.

Several assignments and comparisons use magic string literals ("True", "Unknown", "False") rather than the exported state constants defined in the git package. Using the constants ensures type safety and consistency across the codebase.

  • internal/controller/gittarget_controller.go#L205-L207: replace "True" with git.RenderFidelityTrue.
  • internal/controller/gittarget_controller.go#L221-L224: replace "Unknown" with git.RenderFidelityUnknown.
  • internal/controller/gittarget_status_test.go#L128-L128: replace "True" with git.RenderFidelityTrue.
  • internal/controller/gittarget_status_test.go#L159-L161: replace "Unknown" with git.RenderFidelityUnknown.
  • internal/controller/gittarget_status_test.go#L165-L167: replace "False" with git.RenderFidelityFalse.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/gittarget_controller.go` around lines 205 - 207, Replace
the magic render-fidelity state strings with the exported
git.RenderFidelityState constants: use git.RenderFidelityTrue and
git.RenderFidelityUnknown in internal/controller/gittarget_controller.go at
lines 205-207 and 221-224, and use git.RenderFidelityTrue,
git.RenderFidelityUnknown, and git.RenderFidelityFalse in
internal/controller/gittarget_status_test.go at lines 128, 159-161, and 165-167
respectively.
internal/controller/gittarget_status_test.go (1)

153-153: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Follow test naming convention and consider a table-driven approach.

As per coding guidelines, tests should use the TestFunctionName_Scenario naming convention and employ a table-driven approach where appropriate. Consider renaming this test to reflect the scenario (e.g., TestDeriveGitTargetDataPlaneStatusWithRenderFidelity_AllStates) and refactoring the sequential assertions into a table-driven structure to clearly map each RenderFidelityState to its expected outcomes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/gittarget_status_test.go` at line 153, Rename
TestDeriveGitTargetDataPlaneStatusWithRenderFidelity to follow the
TestFunctionName_Scenario convention, using an all-states scenario name.
Refactor its sequential assertions into a table-driven test that maps each
RenderFidelityState to the expected status outcomes.

Source: Coding guidelines

internal/manifestanalyzer/render_fidelity_test.go (1)

93-93: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove redundant unstructured.Unstructured wrapper.

Wrapping the object map in &unstructured.Unstructured{Object: object} and immediately accessing .Object returns the exact same map reference. You can return object directly.

💡 Proposed refactor
-	return (&unstructured.Unstructured{Object: object}).Object
+	return object
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/manifestanalyzer/render_fidelity_test.go` at line 93, Update the
return statement in the surrounding helper to return object directly, removing
the redundant unstructured.Unstructured wrapper and immediate .Object access
while preserving the same map reference.
internal/watch/event_router.go (1)

225-227: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use static messages for structured logging. As flagged by static analysis, concatenating dynamic variables like kind into the primary log message string is an anti-pattern in structured logging. Keeping the primary log message static while passing dynamic variables as fields improves log indexability and safeguards against log injection.

  • internal/watch/event_router.go#L225-L227: Replace "per-type "+kind+" applied" with a static string (e.g., "per-type applied") and add "kind", kind to the logging arguments.
  • internal/watch/event_router.go#L259-L260: Replace "per-type "+kind+" found a render-vs-live divergence" with a static string and add "kind", kind to the logging arguments.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/watch/event_router.go` around lines 225 - 227, Update both logging
sites in internal/watch/event_router.go: in the per-type applied log at lines
225-227 and the render-vs-live divergence log at lines 259-260, use static
primary messages and add the dynamic kind value as a structured "kind" field
alongside the existing fields.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/design/support-boundary/orchestrator-reconcile-trigger.md`:
- Around line 194-195: Update the orchestrator apply barrier around the Rev→Git
“land pending intent” step to require effective-render fidelity before treating
drift as a no-op. Resume processing only when the applied result matches the
local render; otherwise return a named refusal such as TransformedOutOfBand,
preventing externally injected Flux/Argo transformations from being projected
into source templates.
- Around line 221-229: Define fail-closed handling for the finalize-and-push,
orchestrator trigger, and reconcile-wait flow: on any finalization or push
failure, rejected trigger, timeout, or stale/older reported revision, keep
live-event processing paused and do not resume until pending intent is durably
present in the remote state and the expected orchestrator revision has been
applied.

In `@internal/git/render_fidelity_gate.go`:
- Around line 195-221: Update reduceRenderFidelity so it evaluates every scope
and counts all finished clean scopes before returning a divergence status.
Preserve the existing sorted traversal and divergence details, but defer the
RenderFidelityFalse return until the loop completes, retaining the first
divergence for the returned status and accurate CleanScopes.

---

Nitpick comments:
In `@internal/controller/gittarget_controller.go`:
- Around line 205-207: Replace the magic render-fidelity state strings with the
exported git.RenderFidelityState constants: use git.RenderFidelityTrue and
git.RenderFidelityUnknown in internal/controller/gittarget_controller.go at
lines 205-207 and 221-224, and use git.RenderFidelityTrue,
git.RenderFidelityUnknown, and git.RenderFidelityFalse in
internal/controller/gittarget_status_test.go at lines 128, 159-161, and 165-167
respectively.

In `@internal/controller/gittarget_status_test.go`:
- Line 153: Rename TestDeriveGitTargetDataPlaneStatusWithRenderFidelity to
follow the TestFunctionName_Scenario convention, using an all-states scenario
name. Refactor its sequential assertions into a table-driven test that maps each
RenderFidelityState to the expected status outcomes.

In `@internal/manifestanalyzer/render_fidelity_test.go`:
- Line 93: Update the return statement in the surrounding helper to return
object directly, removing the redundant unstructured.Unstructured wrapper and
immediate .Object access while preserving the same map reference.

In `@internal/watch/event_router.go`:
- Around line 225-227: Update both logging sites in
internal/watch/event_router.go: in the per-type applied log at lines 225-227 and
the render-vs-live divergence log at lines 259-260, use static primary messages
and add the dynamic kind value as a structured "kind" field alongside the
existing fields.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ce97c339-ca90-4161-b714-7228210b1a6e

📥 Commits

Reviewing files that changed from the base of the PR and between 9bf0425 and 039e792.

📒 Files selected for processing (64)
  • .coverage-baseline
  • api/v1alpha3/gittarget_types.go
  • config/crd/bases/configbutler.ai_gittargets.yaml
  • docs/design/support-boundary/README.md
  • docs/design/support-boundary/next-prompt-render-matches-live-gate.md
  • docs/design/support-boundary/orchestrator-reconcile-trigger.md
  • docs/design/support-boundary/render-fidelity-scenarios.md
  • docs/design/support-boundary/render-fidelity.md
  • docs/facts/kustomize-never-emits-dollar-brace.md
  • internal/controller/constants.go
  • internal/controller/gittarget_controller.go
  • internal/controller/gittarget_status_test.go
  • internal/controller/stream_status.go
  • internal/git/branch_worker.go
  • internal/git/git_path_refusal.go
  • internal/git/plan_flush.go
  • internal/git/render_fidelity_gate.go
  • internal/git/render_fidelity_gate_test.go
  • internal/git/render_fidelity_test.go
  • internal/git/worker_manager.go
  • internal/manifestanalyzer/acceptance.go
  • internal/manifestanalyzer/analyzer.go
  • internal/manifestanalyzer/analyzer_test.go
  • internal/manifestanalyzer/render_fidelity.go
  • internal/manifestanalyzer/render_fidelity_test.go
  • internal/manifestanalyzer/testdata/render-fidelity/comment-only-token/git/configmap.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/comment-only-token/live.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/comment-only-token/want.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/label-injects-render-token/git/configmap.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/label-injects-render-token/git/kustomization.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/label-injects-render-token/live.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/label-injects-render-token/want.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/label-overwrites-source-token/git/configmap.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/label-overwrites-source-token/git/kustomization.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/label-overwrites-source-token/live.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/label-overwrites-source-token/want.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/literal-crd-description/git/crd.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/literal-crd-description/live.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/literal-crd-description/want.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/literal-kro-template/git/resourcegraphdefinition.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/literal-kro-template/live.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/literal-kro-template/want.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/literal-nginx-config/git/configmap.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/literal-nginx-config/live.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/literal-nginx-config/want.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/native-dollar-paren/git/configmap.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/native-dollar-paren/live.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/native-dollar-paren/want.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/nested-list-token/git/deployment.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/nested-list-token/live.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/nested-list-token/want.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/plain-postbuild-token/git/deployment.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/plain-postbuild-token/live.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/plain-postbuild-token/want.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/token-field-removed-live/git/configmap.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/token-field-removed-live/live.yaml
  • internal/manifestanalyzer/testdata/render-fidelity/token-field-removed-live/want.yaml
  • internal/watch/event_router.go
  • internal/watch/event_router_test.go
  • internal/watch/git_path_acceptance.go
  • internal/watch/git_path_acceptance_test.go
  • internal/watch/manager.go
  • internal/watch/render_fidelity_gate.go
  • internal/watch/target_watch.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/manifestanalyzer/analyzer_test.go
  • docs/design/support-boundary/README.md

Comment on lines +194 to +195
Note over Rev: detects the remote moved —<br/>holds live events back, replaying-gated FIFO
Rev->>Git: land pending intent — finalize the open window,<br/>push pendingWrites rebased onto the new tip<br/>(replay — a no-op if the drift already matches)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Require render-fidelity before treating the apply as a no-op.

This barrier assumes the orchestrator’s apply matches the local render, but Flux/Argo can add images, replicas, patches, namespaces, labels, suffixes, components, or substitutions unavailable locally. Those values could be misclassified as user edits and projected into source templates. Require an effective-render match or a named refusal such as TransformedOutOfBand before resuming processing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/design/support-boundary/orchestrator-reconcile-trigger.md` around lines
194 - 195, Update the orchestrator apply barrier around the Rev→Git “land
pending intent” step to require effective-render fidelity before treating drift
as a no-op. Resume processing only when the applied result matches the local
render; otherwise return a named refusal such as TransformedOutOfBand,
preventing externally injected Flux/Argo transformations from being projected
into source templates.

Comment on lines +221 to +229
operator may hold **not-yet-remote** intent in two places: the **open commit window** (`openWindow` —
events coalesced in memory but not yet committed) and the **committed-but-unpushed queue**
(`pendingWrites`). Triggering the orchestrator reconcile *now* would apply the new origin over those
live edits on the cluster, erasing them before they reach Git.

So the ordering must be: **land pending intent to Git first, *then* trigger the reconcile, *then*
resume.** "Landing" it is the ordinary finalize-then-push — the open window is finalized into a commit,
joins `pendingWrites`, and the whole queue is pushed, rebased onto the new tip by the replay above (and
a no-op if the drift already matches). This is only safe because the intent is durably recoverable: the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Define fail-closed behavior for barrier failures.

Specify what happens when finalization or push fails, the trigger is rejected, or the orchestrator times out or reports an older revision. Live-event processing must remain paused until pending intent is durably represented in the remote state and the expected orchestrator revision is applied; otherwise the stale-baseline and reconcile-echo hazards described here can reappear.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/design/support-boundary/orchestrator-reconcile-trigger.md` around lines
221 - 229, Define fail-closed handling for the finalize-and-push, orchestrator
trigger, and reconcile-wait flow: on any finalization or push failure, rejected
trigger, timeout, or stale/older reported revision, keep live-event processing
paused and do not resume until pending intent is durably present in the remote
state and the expected orchestrator revision has been applied.

Comment on lines +195 to +221
func reduceRenderFidelity(state renderFidelityTargetState) RenderFidelityStatus {
keys := make([]string, 0, len(state.scopes))
for key := range state.scopes {
keys = append(keys, key)
}
sort.Strings(keys)
clean := 0
for _, key := range keys {
result := state.scopes[key]
if !result.finished {
continue
}
if result.divergence != nil {
sample := *result.divergence
return RenderFidelityStatus{
Epoch: state.epoch,
State: RenderFidelityFalse,
Reason: "RenderDoesNotMatchLive",
Message: "Rendered token " + sample.Token + " at " + sample.Field + " does not match live",
Divergence: &sample,
ScopeCount: len(state.scopes), CleanScopes: clean,
}
}
if result.clean {
clean++
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix CleanScopes undercounting on divergence.

When a divergence is found, the loop exits early, causing CleanScopes to only reflect the number of clean scopes that happened to sort alphabetically before the divergent one, rather than the true count of evaluated clean scopes.

Evaluate all scopes to ensure an accurate count before returning.

🐛 Proposed fix
 func reduceRenderFidelity(state renderFidelityTargetState) RenderFidelityStatus {
 	keys := make([]string, 0, len(state.scopes))
 	for key := range state.scopes {
 		keys = append(keys, key)
 	}
 	sort.Strings(keys)
 	clean := 0
+	var firstDivergence *manifestanalyzer.RenderDivergence
 	for _, key := range keys {
 		result := state.scopes[key]
 		if !result.finished {
 			continue
 		}
-		if result.divergence != nil {
-			sample := *result.divergence
-			return RenderFidelityStatus{
-				Epoch:      state.epoch,
-				State:      RenderFidelityFalse,
-				Reason:     "RenderDoesNotMatchLive",
-				Message:    "Rendered token " + sample.Token + " at " + sample.Field + " does not match live",
-				Divergence: &sample,
-				ScopeCount: len(state.scopes), CleanScopes: clean,
-			}
-		}
+		if result.divergence != nil && firstDivergence == nil {
+			firstDivergence = result.divergence
+		}
 		if result.clean {
 			clean++
 		}
 	}
+	if firstDivergence != nil {
+		sample := *firstDivergence
+		return RenderFidelityStatus{
+			Epoch:      state.epoch,
+			State:      RenderFidelityFalse,
+			Reason:     "RenderDoesNotMatchLive",
+			Message:    "Rendered token " + sample.Token + " at " + sample.Field + " does not match live",
+			Divergence: &sample,
+			ScopeCount: len(state.scopes), CleanScopes: clean,
+		}
+	}
 	if clean != len(state.scopes) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func reduceRenderFidelity(state renderFidelityTargetState) RenderFidelityStatus {
keys := make([]string, 0, len(state.scopes))
for key := range state.scopes {
keys = append(keys, key)
}
sort.Strings(keys)
clean := 0
for _, key := range keys {
result := state.scopes[key]
if !result.finished {
continue
}
if result.divergence != nil {
sample := *result.divergence
return RenderFidelityStatus{
Epoch: state.epoch,
State: RenderFidelityFalse,
Reason: "RenderDoesNotMatchLive",
Message: "Rendered token " + sample.Token + " at " + sample.Field + " does not match live",
Divergence: &sample,
ScopeCount: len(state.scopes), CleanScopes: clean,
}
}
if result.clean {
clean++
}
}
sort.Strings(keys)
clean := 0
var firstDivergence *manifestanalyzer.RenderDivergence
for _, key := range keys {
result := state.scopes[key]
if !result.finished {
continue
}
if result.divergence != nil && firstDivergence == nil {
firstDivergence = result.divergence
}
if result.clean {
clean++
}
}
if firstDivergence != nil {
sample := *firstDivergence
return RenderFidelityStatus{
Epoch: state.epoch,
State: RenderFidelityFalse,
Reason: "RenderDoesNotMatchLive",
Message: "Rendered token " + sample.Token + " at " + sample.Field + " does not match live",
Divergence: &sample,
ScopeCount: len(state.scopes), CleanScopes: clean,
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/git/render_fidelity_gate.go` around lines 195 - 221, Update
reduceRenderFidelity so it evaluates every scope and counts all finished clean
scopes before returning a divergence status. Preserve the existing sorted
traversal and divergence details, but defer the RenderFidelityFalse return until
the loop completes, retaining the first divergence for the returned status and
accurate CleanScopes.

@sunib
sunib merged commit 8444cd0 into main Jul 15, 2026
6 of 7 checks passed
@sunib
sunib deleted the fix/kustomize-source-form-projection branch July 15, 2026 11:57
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