fix(analyzer)!: a refusal that can be fixed must not call itself unsupported, and one encoder for the mirror - #290
Conversation
The queue was built bottom-up: Tier 0 and Tier 1 are still unbuilt while the largest Tier 2 item shipped anyway, together with a consumer ask (#23) that was filed and fixed before it was ever ranked. Record that rather than smoothing it into a re-ranking. Struck: the fact stream, #23, the name tier, metrics Phase 1. Ranked in their place: the removal-wait decision (F then C, Tier 1, because a watched type the audit policy excludes is #15's failure mode one level down), the head-of-line block on the shard goroutine, the aggregated create, and the stream's two capacity questions. #5 loses one of its two arguments and says so: "audit cannot attribute a finalized delete at all" is false now that the sticky removal pointer does exactly that. The first argument — a hosted control plane will not give you an audit webhook — carries it alone. Also answers what "Event" meant in the inference deletion's open question: a real corev1.Event, which is not nearly free there because placement runs on the branch worker rather than in a controller, must attach to the GitTarget rather than to an object that may live in a remote cluster, and is a notification rather than a record. The durable half belongs to B2.
…pported Consumer ask #22, all three parts. Each one was a sentence, and each one was misleading a consumer that reads our source as the contract. ReasonRefusedStructural documented itself as "the permanent support boundary". It has not been permanent since Solvable shipped: the same code answers both ways, correctly, because a root refused for a configMapGenerator is nobody's to solve while one whose kustomization.yaml does not parse is one commit from adoptable. A consumer wrote Permanent = true off that sentence and shipped "this can never be synced" to the second author. The doc now points at Solvable. The refusal detail had one stem for both branches, so a solvable refusal read "kustomization uses unsupported feature(s): unparseable" — the opposite of what Solvable said beside it. The stem now comes from the same classification that sets Solvable, so the two cannot drift the way the doc comment did. The not-solvable stem is byte-identical to what it was, which is why the corpus baseline moves on exactly one refusal: the one that was lying. The nested-kustomization message shares that stem, and stops listing every construct it might have been in favour of the ones it found. Actor now states which scans can report which values. The ask asked whether a structure-only scan can name the platform operator; it cannot, and the reason is not the one reported. InScope gates only IssueOutOfScope; IssueUnresolvedKRM carries the same actor and is gated on a type registry that has seen a cluster. Cluster-awareness is the real gate, which is what makes the guarantee safe to state, and a corpus test pins it. BREAKING CHANGE: the refusal detail for a solvable render-root refusal and the message on an unsupported-kustomize issue are reworded. Both are documented as unstable strings; no code switches on them.
…field Consumer ask #11, filed as cosmetic and accepted as not. A create serialized through sigs.k8s.io/yaml and an update through gopkg.in/yaml.v3, so sequences landed at the parent key's column in one and two columns deeper in the other, and the first update after a create rewrote every list line in the file to carry one changed field. For a product whose entire output is a Git diff, that is not ugliness, it is illegibility. Built in the order the queue asked for: a test pinning create and update output byte-for-byte first, then the fix wherever it failed. The direction was forced rather than chosen. yaml.v3 always indents a sequence under its mapping key, so the create style cannot be produced by the encoder that edits documents in place; create moved to yaml.v3. It is also what every file in a repository the operator has already updated once looks like. The fix is a package and not a shared constant: two encoders agreeing on an indent width are one refactor from diverging again, silently, because both emit valid YAML and nothing fails. internal/yamlstyle is the only place the write path constructs an encoder, and a source-scanning test refuses a second one. It also contains one behaviour difference that had to be absorbed: yaml.v3 raises "cannot marshal type" by panicking with a bare string, which escapes its own recover, where the JSON path returned an error. A write path that returns an error retries and reports; one that panics takes the process down. Adding a key still differs between the two paths, deliberately: a patch appends it where the document's own order puts it while a canonical render sorts keys, and preserving a human's key order is what the in-place editor is for.
Tier 0 and the one Tier 3 entry worth doing beside it are built, so they leave the queue. Both entries keep their argument and gain what the work found: #22's Actor answer was better than the doc line asked for — cluster-awareness is the gate, not AcceptancePolicy.InScope, and the second raise site the consumer's trace missed is the one that proves it. #11 records three things that only came out of doing it: the direction was forced by yaml.v3's sequence indentation rather than chosen, the fix had to be a package rather than a shared constant, and one panic-versus-error difference had to be absorbed at the seam.
|
Warning Review limit reached
Next review available in: 1 minute Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (16)
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. Comment |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Closes the consumer's Tier 0 asks (#22) and the one Tier 3 ask that was too silly to keep around (#11), and reconciles the work queue with what the attribution branch already shipped.
#22 — the analyzer contract said three things that are not true
Three sentences, each misleading a consumer that reads our source as the contract.
ReasonRefusedStructuraladvertised itself as "the permanent support boundary". It has not been permanent sinceSolvableshipped: the same code answers both ways, and correctly — a root refused for aconfigMapGeneratoris nobody's to solve, one whosekustomization.yamldoes not parse is one commit from adoptable. The consumer wrotePermanent = trueoff that sentence and shipped "this can never be synced" to the second author. The doc now points atSolvable.kustomization uses unsupported feature(s): unparseable— the opposite of whatSolvablesaid beside it. The stem now derives from the same classification that setsSolvable, so the two cannot drift the way the doc comment did. The not-solvable stem is byte-identical to before, which is why the corpus baseline moves on exactly one refusal: the one that was lying. The nested-kustomization message shares that stem and stops listing every construct it might have been in favour of the ones it found.Actornow states which scans can report which values — and the answer is better than the doc line that was asked for.InScopegates onlyIssueOutOfScope;IssueUnresolvedKRMcarries the same actor and is gated on a type registry that has seen a cluster. Cluster-awareness is the real gate, which is what makes the guarantee safe to state rather than merely true today, and a corpus test pins that no structure-only scan names the platform operator.#11 — one encoder, and the "cosmetic" label was wrong
A create serialized through
sigs.k8s.io/yamland an update throughgopkg.in/yaml.v3, so sequences landed at the parent key's column in one and two columns deeper in the other. The first update after a create rewrote every list line in the file to carry one changed field. For a product whose entire output is a Git diff, that is illegibility, not ugliness.Built in the order the queue asked for: the byte-for-byte pinning test first, then the fix wherever it failed. Three things came out of doing it —
internal/yamlstyleis the only place the write path constructs an encoder, and a source-scanning test refuses a second one.cannot marshal typeby panicking with a bare string, which escapes its own recover, where the JSON path returned an error. A write path that returns an error retries and reports; one that panics takes the process down.sigs.k8s.io/yamlstays where it parses or round-trips a Go struct through its JSON tags (the analyzer report, the in-memory kustomization copies handed to kustomize) — none of those bytes reach a mirror. One style authority for Git output, not one library for the tree.Adding a key still differs between create and update, deliberately: a patch appends it where the document's own order puts it while a canonical render sorts keys, and preserving a human's key order is what the in-place editor is for. The test says so, so nobody reads it as a gap.
The queue
docs/design/open-asks-priority.mdis reconciled to reality. The fact stream, ask #23, the name tier and metrics Phase 1 shipped and are struck from it; the residue they leave is ranked (the removal-wait decision, the head-of-line block on the shard, the aggregated create). #5 loses one of its two arguments and says so — "audit cannot attribute a finalized delete at all" is false now that the sticky removal pointer does exactly that. It also answers what "Event" meant in the inference-deletion open question: a realcorev1.Event, which is not nearly free there because placement runs on the branch worker rather than in a controller, must attach to the GitTarget rather than to an object that may live in a remote cluster, and is a notification rather than a record — the durable half belongs to B2.Compatibility
Two reworded strings, both documented as unstable and neither switched on by any code: the refusal detail for a solvable render-root refusal, and the
unsupported-kustomizeissue message. No API, CRD, or metric change.No new diff churn from the encoder change: files are re-serialized only when they are written, and an update already re-indented them. What changes is that a create no longer plants a file its own next update will rewrite.
Validation
task lint,task test(coverage 78.5%, baseline held),task lint-docs, and the corpus baseline regenerated (task gitops-layouts-baseline, two lines moved).task test-e2eresult is reported in a comment below.🤖 Generated with Claude Code