From d1d6a0576905643c70ded3a4c6e50cd4e1e2e623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=86gir=20M=C3=A1ni=20Hauksson?= <54936225+sourcehawk@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:28:50 +0200 Subject: [PATCH] docs: use typed primitives in the guard example and pin plugin installs The declared-data guard example in docs/component.md was still built on static.NewBuilder and unstructured field access. That shape predates typed data cells: it was written for the old WithDataExtractor callback, where an unstructured object was the point, and the rewrite to cells and WithDataGuard kept the surrounding scaffolding rather than reshaping it. Every primitive package exports ExtractInto, WithDataGuard, and WithOptionalData, so nothing about declared data needs the unstructured variant. The example now uses a ConfigMap producer and a Deployment consumer, matching the configmap.ExtractInto snippet above it and the deployment.NewBuilder guard example below it, and it drops uns.NestedString and EditContent/SetNestedString in favour of cm.Data and EnsureContainerEnvVar. The unstructured version of the pattern remains in docs/primitives/unstructured.md, where the generic path is the subject. The README gains a team-install snippet for the ocf plugin. Consumers can commit extraKnownMarketplaces and enabledPlugins to their own .claude/settings.json and pin ref to the framework tag their go.mod requires, so the skills Claude reads describe the same API they compile against. A marketplace source accepts ref but not sha, so a release tag is the available pin; without it the marketplace tracks the default branch and the guidance drifts ahead of the pinned release. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PnvBNAXNx7LZyyHR8ZamMU --- README.md | 27 ++++++++++++++++++- docs/component.md | 17 +++++------- .../references/component.md | 17 +++++------- 3 files changed, 40 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 570f2ac1..45156186 100644 --- a/README.md +++ b/README.md @@ -125,13 +125,38 @@ The repository ships a [Claude Code](https://code.claude.com) plugin that teache idioms: skills for components, primitives, custom resource wrappers, operator structure, and testing, plus scaffolding commands and a guidelines reviewer. -Install it from this repository: +Install it for yourself from this repository: ``` /plugin marketplace add sourcehawk/operator-component-framework /plugin install ocf ``` +To share it with everyone working on your operator, commit the marketplace and plugin to your repository's +`.claude/settings.json`. Pin `ref` to the framework tag your `go.mod` requires, so the skills Claude reads describe the +same API you compile against: + +```json +{ + "extraKnownMarketplaces": { + "operator-component-framework": { + "source": { + "source": "github", + "repo": "sourcehawk/operator-component-framework", + "ref": "v0.18.0" + } + } + }, + "enabledPlugins": { + "ocf@operator-component-framework": true + } +} +``` + +Collaborators are prompted to install the plugin the first time they trust the project folder. Bump `ref` in the same +commit that bumps the framework in `go.mod`; without it the marketplace tracks the default branch and the guidance can +drift ahead of your pinned release. + Then use `/ocf:docs `, `/ocf:new-component`, `/ocf:new-wrapper`, and `/ocf:review` inside your operator project. ## Contributing diff --git a/docs/component.md b/docs/component.md index f8040702..67d3cbf4 100644 --- a/docs/component.md +++ b/docs/component.md @@ -682,10 +682,9 @@ func buildBackendComponent(owner *v1alpha1.WebApp) (*component.Component, error) // First resource: a config source. Once it is applied, the declared // extraction reads a value out of the live object and into the cell. - configBuilder := static.NewBuilder(newBackendConfig(owner)) - static.ExtractInto(configBuilder, endpoint, func(obj uns.Unstructured) (string, error) { - value, _, err := uns.NestedString(obj.Object, "data", "endpoint") - return value, err + configBuilder := configmap.NewBuilder(newBackendConfigMap(owner)) + configmap.ExtractInto(configBuilder, endpoint, func(cm corev1.ConfigMap) (string, error) { + return cm.Data["endpoint"], nil }) configRes, err := configBuilder.Build() if err != nil { @@ -695,18 +694,16 @@ func buildBackendComponent(owner *v1alpha1.WebApp) (*component.Component, error) // Second resource: a consumer that needs the endpoint. The data guard blocks // it until the cell is set earlier in this same reconcile cycle; the mutation // then injects the value at Mutate() time. - consumerBuilder := static.NewBuilder(newBackendConsumer(owner)) + consumerBuilder := deployment.NewBuilder(newBackendDeployment(owner)) consumerBuilder.WithDataGuard(endpoint) - consumerBuilder.WithMutation(unstruct.Mutation{ + consumerBuilder.WithMutation(deployment.Mutation{ Name: "set-endpoint", - Mutate: func(m *unstruct.Mutator) error { + Mutate: func(m *deployment.Mutator) error { value, err := endpoint.Require() if err != nil { return err } - m.EditContent(func(e *editors.UnstructuredContentEditor) error { - return e.SetNestedString(value, "spec", "endpoint") - }) + m.EnsureContainerEnvVar(corev1.EnvVar{Name: "BACKEND_ENDPOINT", Value: value}) return nil }, }) diff --git a/plugin/skills/building-components/references/component.md b/plugin/skills/building-components/references/component.md index f8040702..67d3cbf4 100644 --- a/plugin/skills/building-components/references/component.md +++ b/plugin/skills/building-components/references/component.md @@ -682,10 +682,9 @@ func buildBackendComponent(owner *v1alpha1.WebApp) (*component.Component, error) // First resource: a config source. Once it is applied, the declared // extraction reads a value out of the live object and into the cell. - configBuilder := static.NewBuilder(newBackendConfig(owner)) - static.ExtractInto(configBuilder, endpoint, func(obj uns.Unstructured) (string, error) { - value, _, err := uns.NestedString(obj.Object, "data", "endpoint") - return value, err + configBuilder := configmap.NewBuilder(newBackendConfigMap(owner)) + configmap.ExtractInto(configBuilder, endpoint, func(cm corev1.ConfigMap) (string, error) { + return cm.Data["endpoint"], nil }) configRes, err := configBuilder.Build() if err != nil { @@ -695,18 +694,16 @@ func buildBackendComponent(owner *v1alpha1.WebApp) (*component.Component, error) // Second resource: a consumer that needs the endpoint. The data guard blocks // it until the cell is set earlier in this same reconcile cycle; the mutation // then injects the value at Mutate() time. - consumerBuilder := static.NewBuilder(newBackendConsumer(owner)) + consumerBuilder := deployment.NewBuilder(newBackendDeployment(owner)) consumerBuilder.WithDataGuard(endpoint) - consumerBuilder.WithMutation(unstruct.Mutation{ + consumerBuilder.WithMutation(deployment.Mutation{ Name: "set-endpoint", - Mutate: func(m *unstruct.Mutator) error { + Mutate: func(m *deployment.Mutator) error { value, err := endpoint.Require() if err != nil { return err } - m.EditContent(func(e *editors.UnstructuredContentEditor) error { - return e.SetNestedString(value, "spec", "endpoint") - }) + m.EnsureContainerEnvVar(corev1.EnvVar{Name: "BACKEND_ENDPOINT", Value: value}) return nil }, })