Skip to content

docs(types): make ResourceIdentifier.Key() a stated, tested contract - #273

Merged
sunib merged 2 commits into
mainfrom
feat/analyzer-ask3-key-format-contract
Jul 27, 2026
Merged

docs(types): make ResourceIdentifier.Key() a stated, tested contract#273
sunib merged 2 commits into
mainfrom
feat/analyzer-ask3-key-format-contract

Conversation

@sunib

@sunib sunib commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Implements Ask 3 of docs/design/analyzer-consumer-contract-asks.md: ResourceIdentifier.Key() is an identity two products must agree on, but its format lived in a three-line comment and had no test at all — we could have changed it today and every gate would have stayed green, and a downstream tool that mirrors the struct would have found out late, after a version bump it chose.

What changed

The format is now stated as public. The godoc on Key() names it a contract, spells out the two empty-segment rules that pull in opposite directions, and lists the four shapes:

case key
namespaced, grouped apps/v1/deployments/prod/api
cluster-scoped, grouped rbac.authorization.k8s.io/v1/clusterroles/admin
namespaced, core group /v1/secrets/prod/db
cluster-scoped, core group /v1/nodes/node-1

The two rules are opposites, which is what a reimplementation gets wrong: a cluster-scoped resource drops the namespace segment (always joining five parts yields …/clusterroles//admin, which never joins), while a core-group resource emits its empty group, so the key leads with /. The fourth case applies both at once.

The golden test pins the exact strings, in internal/types/identifier_key_test.go, carrying the sentence that turns a red run from an obstacle into a decision: changing a byte of this format is a breaking change rather than a refactor. Two further tests guard the properties the format exists for — that no two distinct identifiers collide (including the cluster-scoped-name vs. namespace-name case that a dropped segment could conflate), and the version disagreement below.

Two runnable examples (ExampleResourceIdentifier_Key, ExampleResourceIdentifier_ToGitPath) put the same four strings in godoc, so a consumer reading the docs sees the output rather than a description of it, and go test fails if the two ever disagree.

The version-identity decision, recorded beside both methods. The ask asked us to decide which of the two functions is the identity, since Key() includes Version and ToGitPath() deliberately does not:

Key is the API-side identity — correct for in-process map keys, deduplication and logs, where every participant observes one version at a time. The versionless, namespace-first Git path is the durable identity of the object, which is why a storage-version bump moves no file in Git. A join that must survive such a bump keys on the versionless identity, not on Key.

TestResourceIdentifier_Key_VersionSplitsWhereGitPathDoesNot pins that disagreement so the recorded decision cannot quietly stop being true.

Ancillary: ResourceReference.Key() ("namespace/name") now says it is not the other Key() — the ask called the ambiguity out explicitly, and it is the one a reader can land on first.

One line in the pkg/manifestanalyzer package doc points at the format with the four shapes inline, since that is where a consumer of the analyzer looks and internal/types is not importable from another module.

The scan for reimplementations

Swept the tree for places that build a resource identity by hand rather than calling Key(). Exactly one turned up: sensitiveIdentityKey in internal/git/content_writer.go duplicated the format inline and always joined five segments — the naive shape the new godoc warns about. It is a process-local map key for the secret-encryption marker cache, so nothing was wrong today (only namespaced resources reach it in practice, and a trailing empty segment is still unique), but it was a second implementation of a format we are now declaring a contract. Replaced with id.Key().

Everything else that looked adjacent is a genuinely different key and was left alone: PlacementTypeKey and the byType placement key are GVR type keys with no name or namespace, windowPathKey keys on a destination path by design, manifestedit's index key is document-side (apiVersion/kind, not resource), mutationlab.ObjectKey is a struct that also carries subresource and UID, and typeset's sort key is type-only.

Not in this PR

The ask floats one optional extra — a versionless key method beside Key(), so a consumer joining across a storage-version bump has something to mirror instead of building their own. It is cheap, but it would add an exported method with no in-tree caller, so it is a maintainer call rather than an implementation detail: say the word and it goes in this PR. Without it, the decision above is recorded and the consumer drops the second segment themselves, which the godoc now tells them to do.

Validation

task lint, task test (77.7%, within the baseline tolerance) and task test-e2e all pass.

🤖 Generated with Claude Code

sunib and others added 2 commits July 27, 2026 04:52
Key() is an identity two products must agree on — a downstream tool mirrors
the struct and joins its rows against ours — but the format lived in a three
line comment and had no test at all, so we could change it today and every
gate would stay green.

- Godoc names the format as public, spells out the two empty-segment rules
  that pull in opposite directions (a cluster-scoped resource drops the
  namespace segment; a core-group resource emits an empty group and leads
  with "/"), and lists the four resulting shapes.
- A table-driven golden test pins the exact strings for those four shapes,
  carrying the sentence that makes a red run a decision: changing a byte of
  this format is a breaking change rather than a refactor. Two runnable
  examples put the same strings in godoc.
- Records the version-identity decision beside both methods: Key is the
  API-side identity and splits on a preferred-version bump, while the
  versionless ToGitPath is the durable one, which is why such a bump moves
  no file in Git. A test pins that disagreement so it cannot drift.
- Disambiguates ResourceReference.Key ("namespace/name"), the other Key()
  a reader can land on first.
- One line in the pkg/manifestanalyzer package doc points at the format,
  since that is where a consumer of the analyzer looks.

Implements Ask 3 of docs/design/analyzer-consumer-contract-asks.md.

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

Scanning for places that build a resource identity by hand turned up exactly
one: sensitiveIdentityKey duplicated the format inline and always joined five
segments, the naive shape the Key() godoc now warns about. It is a
process-local map key so nothing was wrong today, but it is a second
implementation of a format we just declared a contract.

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

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@sunib, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 12 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a01ae0dd-03f6-41f0-a53c-2aac879d0636

📥 Commits

Reviewing files that changed from the base of the PR and between 1f0b5d1 and 102af9b.

📒 Files selected for processing (5)
  • internal/git/content_writer.go
  • internal/types/identifier.go
  • internal/types/identifier_key_test.go
  • internal/types/reference.go
  • pkg/manifestanalyzer/doc.go
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/analyzer-ask3-key-format-contract

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.

@sunib
sunib merged commit 80989ea into main Jul 27, 2026
18 checks passed
@sunib
sunib deleted the feat/analyzer-ask3-key-format-contract branch July 27, 2026 06:09
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