Skip to content

Code Sample Pinning (samples.yaml) Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add samples.yaml per pack for canonical code sample references, a sap-devs samples CLI command (list/search/open/clone), and opt-in injection into AI tool context.

Architecture: New Sample struct on Pack, loaded/merged like existing YAML arrays (influencers, resources). Content helpers in internal/content/samples.go. CLI command in cmd/samples.go following the resources subcommand pattern. Injectable samples rendered as a ## Canonical Patterns section in render.go.

Tech Stack: Go, cobra, testify, gopkg.in/yaml.v3, github.com/pkg/browser

Spec: docs/superpowers/specs/2026-04-18-samples-design.md


Task 1: Data Model — Sample Struct and Pack Loading

Files:

  • Modify: internal/content/pack.go:14-38 (Pack struct) and internal/content/pack.go:266-280 (LoadPack YAML block)

  • [ ] Step 1: Add the Sample struct to pack.go

After the Influencer struct (line 108), add:

go
// Sample is a canonical code sample reference within a pack.
type Sample struct {
	ID          string   `yaml:"id"`
	Label       string   `yaml:"label"`
	URL         string   `yaml:"url"`
	Description string   `yaml:"description"`
	Tags        []string `yaml:"tags"`
	Inject      bool     `yaml:"inject"`
	PackID      string   // set at load time, not in YAML
}
  • [ ] Step 2: Add Samples field to the Pack struct

In the Pack struct (line 14-38), add after the Influencers field:

go
Samples     []Sample
  • [ ] Step 3: Add samples.yaml loading to LoadPack

In LoadPack(), after the influencers.yaml loading block (lines 248-253), add:

go
if data, err := os.ReadFile(filepath.Join(packDir, "samples.yaml")); err == nil {
	_ = yaml.Unmarshal(data, &pack.Samples)
	for i := range pack.Samples {
		pack.Samples[i].PackID = pack.ID
	}
}
  • [ ] Step 4: Verify build

Run: go build ./... Expected: clean build, no errors

  • [ ] Step 5: Commit
bash
git add internal/content/pack.go
git commit -m "feat(samples): add Sample struct and pack loading"

Task 2: Merge Logic

Files:

  • Modify: internal/content/merge.go:43-51 (structured list merge calls)

  • Modify: internal/content/export_test.go (add test-only export for mergeSamples)

  • [ ] Step 1: Add mergeSamples function

After the mergeEventInstances function (line 227), add:

go
// mergeSamples builds a fresh []Sample: starts with base entries, replaces
// any entry whose ID matches an additive entry, appends unmatched additive entries.
// PackID is re-stamped to packID on every entry in the result.
func mergeSamples(base, additive []Sample, packID string) []Sample {
	result := make([]Sample, len(base))
	copy(result, base)
	for _, a := range additive {
		replaced := false
		for i, b := range result {
			if b.ID == a.ID {
				result[i] = a
				replaced = true
				break
			}
		}
		if !replaced {
			result = append(result, a)
		}
	}
	for i := range result {
		result[i].PackID = packID
	}
	return result
}
  • [ ] Step 2: Wire mergeSamples into MergeWith

In MergeWith(), after line 51 (merged.EventInstances = mergeEventInstances(...)), add:

go
merged.Samples = mergeSamples(base.Samples, a.Samples, base.ID)
  • [ ] Step 3: Verify build

Run: go build ./... Expected: clean build

  • [ ] Step 4: Write merge test

In internal/content/merge_test.go, add after the existing hook merge tests:

go
func TestMergeSamples_ReplacesOnMatchingIDAndRestampsPackID(t *testing.T) {
	base := []content.Sample{
		{ID: "cap/handler", Label: "Old", URL: "https://old.example", PackID: "cap"},
		{ID: "cap/schema", Label: "Schema", URL: "https://schema.example", PackID: "cap"},
	}
	additive := []content.Sample{
		{ID: "cap/handler", Label: "New", URL: "https://new.example", PackID: "company"},
	}
	got := content.MergeSamples(base, additive, "cap")
	assert.Len(t, got, 2)
	assert.Equal(t, "New", got[0].Label)
	assert.Equal(t, "https://new.example", got[0].URL)
	assert.Equal(t, "cap", got[0].PackID)
	assert.Equal(t, "cap/schema", got[1].ID)
}

func TestMergeSamples_AppendsNewIDs(t *testing.T) {
	base := []content.Sample{{ID: "cap/handler", Label: "Handler", PackID: "cap"}}
	additive := []content.Sample{{ID: "cap/new", Label: "New Sample", PackID: "company"}}
	got := content.MergeSamples(base, additive, "cap")
	assert.Len(t, got, 2)
	assert.Equal(t, "cap/new", got[1].ID)
	assert.Equal(t, "cap", got[1].PackID)
}

Note: mergeSamples is unexported. The codebase pattern uses internal/content/export_test.go to re-export unexported functions for the test package. Add this line to export_test.go:

go
func MergeSamples(base, add []Sample, id string) []Sample { return mergeSamples(base, add, id) }

This matches the existing pattern for MergeResources, MergeHooks, MergeMCPServers, and MergeTools in that file.

  • [ ] Step 5: Run tests

Run: go build ./... && go vet ./... Expected: clean

  • [ ] Step 6: Commit
bash
git add internal/content/merge.go internal/content/merge_test.go internal/content/export_test.go
git commit -m "feat(samples): add samples merge logic with tests"

Task 3: Content Helpers

Files:

  • Create: internal/content/samples.go

  • Create: internal/content/samples_test.go

  • [ ] Step 1: Write samples_test.go with all tests

go
package content_test

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/SAP-samples/sap-devs-cli/internal/content"
)

func fixtureSamplePacks() []*content.Pack {
	return []*content.Pack{
		{
			ID: "cap",
			Samples: []content.Sample{
				{ID: "cap/handler", Label: "CAP service handler", URL: "https://github.com/SAP-samples/cloud-cap-samples/blob/main/bookshop/srv/cat-service.js", Description: "Canonical handler pattern", Tags: []string{"cap", "node", "handler"}, Inject: true, PackID: "cap"},
				{ID: "cap/schema", Label: "CDS data model", URL: "https://github.com/SAP-samples/cloud-cap-samples/blob/main/bookshop/db/schema.cds", Description: "Entity definitions", Tags: []string{"cap", "cds"}, Inject: false, PackID: "cap"},
			},
		},
		{
			ID: "abap",
			Samples: []content.Sample{
				{ID: "abap/rap-bo", Label: "RAP Business Object", URL: "https://github.com/SAP-samples/abap-platform-rap/blob/main/src/zbp_travel.clas.abap", Description: "RAP BO implementation", Tags: []string{"abap", "rap"}, Inject: true, PackID: "abap"},
			},
		},
	}
}

func TestFlattenSamples(t *testing.T) {
	got := content.FlattenSamples(fixtureSamplePacks())
	require.Len(t, got, 3)
	assert.Equal(t, "cap/handler", got[0].ID)
	assert.Equal(t, "cap", got[0].PackID)
	assert.Equal(t, "cap/schema", got[1].ID)
	assert.Equal(t, "abap/rap-bo", got[2].ID)
}

func TestFlattenSamples_NilInput(t *testing.T) {
	got := content.FlattenSamples(nil)
	assert.Empty(t, got)
}

func TestFilterSamplesByTags_ORMatch(t *testing.T) {
	samples := content.FlattenSamples(fixtureSamplePacks())
	got := content.FilterSamplesByTags(samples, []string{"rap"})
	require.Len(t, got, 1)
	assert.Equal(t, "abap/rap-bo", got[0].ID)
}

func TestFilterSamplesByTags_MultipleTagsOR(t *testing.T) {
	samples := content.FlattenSamples(fixtureSamplePacks())
	got := content.FilterSamplesByTags(samples, []string{"cds", "rap"})
	require.Len(t, got, 2)
}

func TestFilterSamplesByTags_CaseInsensitive(t *testing.T) {
	samples := content.FlattenSamples(fixtureSamplePacks())
	got := content.FilterSamplesByTags(samples, []string{"CAP"})
	assert.Len(t, got, 2)
}

func TestFilterSamplesByTags_NoMatch(t *testing.T) {
	samples := content.FlattenSamples(fixtureSamplePacks())
	got := content.FilterSamplesByTags(samples, []string{"nonexistent"})
	assert.Empty(t, got)
}

func TestFilterSamplesByPack(t *testing.T) {
	got := content.FilterSamplesByPack(fixtureSamplePacks(), "cap")
	require.Len(t, got, 2)
	assert.Equal(t, "cap/handler", got[0].ID)
}

func TestFilterSamplesByPack_NotFound(t *testing.T) {
	got := content.FilterSamplesByPack(fixtureSamplePacks(), "nonexistent")
	assert.Nil(t, got)
}

func TestFilterSamples_DescriptionMatch(t *testing.T) {
	samples := content.FlattenSamples(fixtureSamplePacks())
	got := content.FilterSamples(samples, "canonical")
	require.Len(t, got, 1)
	assert.Equal(t, "cap/handler", got[0].ID)
}

func TestFilterSamples_TagMatch(t *testing.T) {
	samples := content.FlattenSamples(fixtureSamplePacks())
	got := content.FilterSamples(samples, "rap")
	require.Len(t, got, 1)
	assert.Equal(t, "abap/rap-bo", got[0].ID)
}

func TestFilterSamples_IDMatch(t *testing.T) {
	samples := content.FlattenSamples(fixtureSamplePacks())
	got := content.FilterSamples(samples, "cap/schema")
	require.Len(t, got, 1)
	assert.Equal(t, "cap/schema", got[0].ID)
}

func TestFilterSamples_LabelMatch(t *testing.T) {
	samples := content.FlattenSamples(fixtureSamplePacks())
	got := content.FilterSamples(samples, "RAP Business")
	require.Len(t, got, 1)
	assert.Equal(t, "abap/rap-bo", got[0].ID)
}

func TestFilterSamples_CaseInsensitive(t *testing.T) {
	samples := content.FlattenSamples(fixtureSamplePacks())
	got := content.FilterSamples(samples, "ENTITY")
	require.Len(t, got, 1)
	assert.Equal(t, "cap/schema", got[0].ID)
}

func TestFilterSamples_NoMatch(t *testing.T) {
	samples := content.FlattenSamples(fixtureSamplePacks())
	got := content.FilterSamples(samples, "zzznomatch")
	assert.Empty(t, got)
}

func TestFindSample_Found(t *testing.T) {
	samples := content.FlattenSamples(fixtureSamplePacks())
	got := content.FindSample(samples, "cap/schema")
	require.NotNil(t, got)
	assert.Equal(t, "CDS data model", got.Label)
}

func TestFindSample_NotFound(t *testing.T) {
	samples := content.FlattenSamples(fixtureSamplePacks())
	got := content.FindSample(samples, "nonexistent/id")
	assert.Nil(t, got)
}
  • [ ] Step 2: Run tests to verify they fail

Run: go build ./... Expected: compile error — content.FlattenSamples undefined

  • [ ] Step 3: Write samples.go
go
package content

import "strings"

// FlattenSamples collects all samples from all packs into a single slice.
func FlattenSamples(packs []*Pack) []Sample {
	var out []Sample
	for _, p := range packs {
		out = append(out, p.Samples...)
	}
	return out
}

// FilterSamplesByTags returns samples with at least one tag matching any of the
// provided tags (OR semantics, case-insensitive).
func FilterSamplesByTags(samples []Sample, tags []string) []Sample {
	tagSet := make(map[string]bool, len(tags))
	for _, t := range tags {
		tagSet[strings.ToLower(strings.TrimSpace(t))] = true
	}
	var out []Sample
	for _, s := range samples {
		for _, t := range s.Tags {
			if tagSet[strings.ToLower(t)] {
				out = append(out, s)
				break
			}
		}
	}
	return out
}

// FilterSamplesByPack returns samples from the pack matching the given pack ID.
func FilterSamplesByPack(packs []*Pack, packID string) []Sample {
	for _, p := range packs {
		if p.ID == packID {
			return p.Samples
		}
	}
	return nil
}

// FilterSamples returns samples whose ID, Label, Description, or any Tag
// contains query (case-insensitive substring match).
func FilterSamples(samples []Sample, query string) []Sample {
	q := strings.ToLower(query)
	var out []Sample
	for _, s := range samples {
		if strings.Contains(strings.ToLower(s.ID), q) ||
			strings.Contains(strings.ToLower(s.Label), q) ||
			strings.Contains(strings.ToLower(s.Description), q) {
			out = append(out, s)
			continue
		}
		for _, tag := range s.Tags {
			if strings.Contains(strings.ToLower(tag), q) {
				out = append(out, s)
				break
			}
		}
	}
	return out
}

// FindSample returns a pointer to the first sample with an exact ID match, or nil.
func FindSample(samples []Sample, id string) *Sample {
	for i := range samples {
		if samples[i].ID == id {
			return &samples[i]
		}
	}
	return nil
}
  • [ ] Step 4: Run tests

Run: go build ./... && go vet ./... Expected: clean

  • [ ] Step 5: Commit
bash
git add internal/content/samples.go internal/content/samples_test.go
git commit -m "feat(samples): add content helpers with tests"

Task 4: JSON Schema and VS Code Mapping

Files:

  • Create: content/schemas/samples.schema.json

  • Modify: .vscode/settings.json

  • [ ] Step 1: Create samples.schema.json

json
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Pack Samples",
  "description": "Schema for sap-devs samples.yaml files (top-level array)",
  "type": "array",
  "items": {
    "type": "object",
    "required": ["id", "label", "url", "description", "tags"],
    "additionalProperties": false,
    "properties": {
      "id": {
        "type": "string",
        "pattern": "^[a-z][a-z0-9-]*/[a-z][a-z0-9-]*$",
        "description": "Sample identifier in format <pack-id>/<slug>, e.g. cap/cat-service-handler"
      },
      "label": { "type": "string", "description": "Human-readable sample name" },
      "url": { "type": "string", "format": "uri", "description": "Full URL to the sample file (typically a GitHub file URL)" },
      "description": { "type": "string", "description": "What this sample demonstrates" },
      "tags": {
        "type": "array",
        "items": { "type": "string" },
        "minItems": 1,
        "description": "Filtering tags for discovery (e.g. cap, node, handler)"
      },
      "inject": {
        "type": "boolean",
        "default": false,
        "description": "If true, included in the AI context injection as a canonical pattern"
      }
    }
  }
}
  • [ ] Step 2: Add schema mapping to .vscode/settings.json

Add after the event-instances.schema.json line:

json
"./content/schemas/samples.schema.json": "**/packs/*/samples.yaml"
  • [ ] Step 3: Verify build

Run: go build ./... Expected: clean (schema is data, not code)

  • [ ] Step 4: Commit
bash
git add content/schemas/samples.schema.json .vscode/settings.json
git commit -m "feat(samples): add JSON schema and VS Code mapping"

Task 5: Sample Content Data

Files:

  • Create: content/packs/cap/samples.yaml

  • [ ] Step 1: Create cap/samples.yaml

yaml
- id: cap/cat-service-handler
  label: CAP service handler (Node.js)
  url: https://github.com/SAP-samples/cloud-cap-samples/blob/main/bookshop/srv/cat-service.js
  description: Canonical pattern for before/on/after handlers with draft support
  tags: [cap, node, handler, draft]
  inject: true

- id: cap/custom-logic-java
  label: CAP custom logic (Java)
  url: https://github.com/SAP-samples/cloud-cap-samples-java/blob/main/srv/src/main/java/my/bookshop/handlers/CatalogServiceHandler.java
  description: Java event handler registration with @On/@Before/@After annotations
  tags: [cap, java, handler]
  inject: true

- id: cap/cds-schema
  label: CDS data model
  url: https://github.com/SAP-samples/cloud-cap-samples/blob/main/bookshop/db/schema.cds
  description: Entity definitions with associations, compositions, and managed aspects
  tags: [cap, cds, data-model]
  inject: false

- id: cap/custom-action
  label: CAP custom action (Node.js)
  url: https://github.com/SAP-samples/cloud-cap-samples/blob/main/bookshop/srv/admin-service.js
  description: Bound and unbound action/function implementation pattern
  tags: [cap, node, action, function]
  inject: false

- id: cap/cds-service
  label: CDS service definition
  url: https://github.com/SAP-samples/cloud-cap-samples/blob/main/bookshop/srv/cat-service.cds
  description: Service projection with annotations and access control
  tags: [cap, cds, service]
  inject: true
  • [ ] Step 2: Verify pack loads

Run: SAP_DEVS_DEV=1 go run . resources list (to verify the build still works with the new YAML) Expected: resources listed, no errors about samples.yaml

  • [ ] Step 3: Commit
bash
git add content/packs/cap/samples.yaml
git commit -m "feat(samples): add initial CAP sample data"

Task 6: i18n Strings

Files:

  • Modify: internal/i18n/catalogs/en.json

  • Modify: internal/i18n/catalogs/de.json

  • [ ] Step 1: Add English strings to en.json

After the influencers block (after line 200), add:

json
  "samples.short": "Browse canonical code samples",
  "samples.list.short": "List canonical code samples for your active profile",
  "samples.search.short": "Search across all code samples",
  "samples.open.short": "Open a sample URL in the default browser",
  "samples.clone.short": "Clone the sample's repository to the current directory",
  "samples.list.no_profile": "no profile set — run 'sap-devs profile set <name>' first",
  "samples.list.profile_not_found": "profile \"{{.ID}}\" not found — run 'sap-devs sync' to refresh content",
  "samples.none": "No samples found for your current profile.",
  "samples.none_pack": "No samples found for pack \"{{.Pack}}\".",
  "samples.none_tags": "No samples match tags {{.Tags}}.",
  "samples.search.no_results": "No samples found matching \"{{.Query}}\".",
  "samples.not_found": "Sample \"{{.ID}}\" not found — use 'sap-devs samples list' or 'sap-devs samples search' to browse",
  "samples.open.browser_fail": "Could not open browser: {{.Err}}. URL: {{.URL}}",
  "samples.open.opening": "Opening: {{.Label}} — {{.URL}}",
  "samples.clone.not_github": "URL is not a GitHub repository URL. Use 'sap-devs samples open {{.ID}}' to view it in a browser instead.",
  "samples.clone.exists": "Directory \"{{.Dir}}\" already exists — skipping clone.",
  "samples.clone.cloning": "Cloning {{.Repo}}...",
  "samples.clone.done": "Cloned to {{.Dir}}",
  "samples.col_id": "ID",
  "samples.col_pack": "PACK",
  "samples.col_label": "LABEL",
  "samples.col_tags": "TAGS",
  • [ ] Step 2: Add German strings to de.json

After the influencers block (same position), add:

json
  "samples.short": "Kanonische Code-Beispiele durchsuchen",
  "samples.list.short": "Kanonische Code-Beispiele für aktives Profil auflisten",
  "samples.search.short": "Über alle Code-Beispiele suchen",
  "samples.open.short": "Beispiel-URL im Standardbrowser öffnen",
  "samples.clone.short": "Repository des Beispiels in das aktuelle Verzeichnis klonen",
  "samples.list.no_profile": "Kein Profil gesetzt — 'sap-devs profile set <name>' zuerst ausführen",
  "samples.list.profile_not_found": "Profil \"{{.ID}}\" nicht gefunden — 'sap-devs sync' ausführen, um Inhalte zu aktualisieren",
  "samples.none": "Keine Beispiele für dein aktuelles Profil gefunden.",
  "samples.none_pack": "Keine Beispiele für Pack \"{{.Pack}}\" gefunden.",
  "samples.none_tags": "Keine Beispiele für Tags {{.Tags}} gefunden.",
  "samples.search.no_results": "Keine Beispiele gefunden, die \"{{.Query}}\" entsprechen.",
  "samples.not_found": "Beispiel \"{{.ID}}\" nicht gefunden — 'sap-devs samples list' oder 'sap-devs samples search' zum Durchsuchen verwenden",
  "samples.open.browser_fail": "Browser konnte nicht geöffnet werden: {{.Err}}. URL: {{.URL}}",
  "samples.open.opening": "Öffne: {{.Label}} — {{.URL}}",
  "samples.clone.not_github": "URL ist keine GitHub-Repository-URL. 'sap-devs samples open {{.ID}}' verwenden, um es im Browser anzuzeigen.",
  "samples.clone.exists": "Verzeichnis \"{{.Dir}}\" existiert bereits — Klonen übersprungen.",
  "samples.clone.cloning": "Klone {{.Repo}}...",
  "samples.clone.done": "Geklont nach {{.Dir}}",
  "samples.col_id": "ID",
  "samples.col_pack": "PACK",
  "samples.col_label": "BEZEICHNUNG",
  "samples.col_tags": "TAGS",
  • [ ] Step 3: Verify build

Run: go build ./... Expected: clean (embedded catalogs compile fine)

  • [ ] Step 4: Commit
bash
git add internal/i18n/catalogs/en.json internal/i18n/catalogs/de.json
git commit -m "feat(samples): add i18n strings for samples command"

Task 7: CLI Command — list, search, open

Files:

  • Create: cmd/samples.go

  • Note: cmd/root.go is NOT modified — cobra's init() pattern with rootCmd.AddCommand(samplesCmd) in cmd/samples.go handles registration automatically, same as all other command files.

  • [ ] Step 1: Create cmd/samples.go with list, search, and open

go
package cmd

import (
	"fmt"
	"strings"

	"github.com/pkg/browser"
	"github.com/spf13/cobra"
	"github.com/SAP-samples/sap-devs-cli/internal/config"
	"github.com/SAP-samples/sap-devs-cli/internal/content"
	"github.com/SAP-samples/sap-devs-cli/internal/i18n"
	"github.com/SAP-samples/sap-devs-cli/internal/xdg"
)

var (
	samplesAll  bool
	samplesPack string
	samplesTags string
)

var samplesCmd = &cobra.Command{
	Use:   "samples",
	Short: i18n.T("en", "samples.short"),
}

var samplesListCmd = &cobra.Command{
	Use:   "list",
	Short: i18n.T("en", "samples.list.short"),
	RunE: func(cmd *cobra.Command, args []string) error {
		loader, err := newContentLoader()
		if err != nil {
			return err
		}

		var packs []*content.Pack

		if samplesPack != "" || samplesAll {
			packs, err = loader.LoadPacks(nil, i18n.ActiveLang)
			if err != nil {
				return err
			}
		} else {
			paths, err := xdg.New()
			if err != nil {
				return err
			}
			profileCfg, err := config.LoadProfile(paths.ConfigDir)
			if err != nil {
				return err
			}
			if profileCfg.ID == "" {
				return fmt.Errorf("%s", i18n.T(i18n.ActiveLang, "samples.list.no_profile"))
			}
			activeProfile, err := loader.FindProfile(profileCfg.ID)
			if err != nil {
				return err
			}
			if activeProfile == nil {
				return fmt.Errorf("%s", i18n.Tf(i18n.ActiveLang, "samples.list.profile_not_found", map[string]any{"ID": profileCfg.ID}))
			}
			packs, err = loader.LoadPacks(activeProfile, i18n.ActiveLang)
			if err != nil {
				return err
			}
		}

		var samples []content.Sample
		if samplesPack != "" {
			samples = content.FilterSamplesByPack(packs, samplesPack)
		} else {
			samples = content.FlattenSamples(packs)
		}

		if samplesTags != "" {
			tags := strings.Split(samplesTags, ",")
			samples = content.FilterSamplesByTags(samples, tags)
		}

		if len(samples) == 0 {
			if samplesPack != "" {
				fmt.Fprintln(cmd.OutOrStdout(), i18n.Tf(i18n.ActiveLang, "samples.none_pack", map[string]any{"Pack": samplesPack}))
			} else if samplesTags != "" {
				fmt.Fprintln(cmd.OutOrStdout(), i18n.Tf(i18n.ActiveLang, "samples.none_tags", map[string]any{"Tags": samplesTags}))
			} else {
				fmt.Fprintln(cmd.OutOrStdout(), i18n.T(i18n.ActiveLang, "samples.none"))
			}
			return nil
		}
		printSampleTable(samples, false)
		return nil
	},
}

var samplesSearchCmd = &cobra.Command{
	Use:   "search <query>",
	Short: i18n.T("en", "samples.search.short"),
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		loader, err := newContentLoader()
		if err != nil {
			return err
		}
		packs, err := loader.LoadPacks(nil, i18n.ActiveLang)
		if err != nil {
			return err
		}
		samples := content.FilterSamples(content.FlattenSamples(packs), args[0])
		if len(samples) == 0 {
			fmt.Fprintln(cmd.OutOrStdout(), i18n.Tf(i18n.ActiveLang, "samples.search.no_results", map[string]any{"Query": args[0]}))
			return nil
		}
		printSampleTable(samples, true)
		return nil
	},
}

var samplesOpenCmd = &cobra.Command{
	Use:   "open <id>",
	Short: i18n.T("en", "samples.open.short"),
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		loader, err := newContentLoader()
		if err != nil {
			return err
		}
		packs, err := loader.LoadPacks(nil, i18n.ActiveLang)
		if err != nil {
			return err
		}
		s := content.FindSample(content.FlattenSamples(packs), args[0])
		if s == nil {
			return fmt.Errorf("%s", i18n.Tf(i18n.ActiveLang, "samples.not_found", map[string]any{"ID": args[0]}))
		}
		if err := browser.OpenURL(s.URL); err != nil {
			fmt.Fprintln(cmd.OutOrStdout(), i18n.Tf(i18n.ActiveLang, "samples.open.browser_fail", map[string]any{"Err": err, "URL": s.URL}))
			return nil
		}
		fmt.Fprintln(cmd.OutOrStdout(), i18n.Tf(i18n.ActiveLang, "samples.open.opening", map[string]any{"Label": s.Label, "URL": s.URL}))
		return nil
	},
}

func printSampleTable(samples []content.Sample, showPack bool) {
	colID := i18n.T(i18n.ActiveLang, "samples.col_id")
	colPack := i18n.T(i18n.ActiveLang, "samples.col_pack")
	colLabel := i18n.T(i18n.ActiveLang, "samples.col_label")
	colTags := i18n.T(i18n.ActiveLang, "samples.col_tags")
	if showPack {
		fmt.Printf("%-35s %-12s %-35s %s\n", colID, colPack, colLabel, colTags)
		fmt.Println(strings.Repeat("-", 95))
		for _, s := range samples {
			fmt.Printf("%-35s %-12s %-35s %s\n", s.ID, s.PackID, s.Label, strings.Join(s.Tags, ","))
		}
	} else {
		fmt.Printf("%-35s %-35s %s\n", colID, colLabel, colTags)
		fmt.Println(strings.Repeat("-", 80))
		for _, s := range samples {
			fmt.Printf("%-35s %-35s %s\n", s.ID, s.Label, strings.Join(s.Tags, ","))
		}
	}
}

func init() {
	samplesListCmd.Flags().BoolVarP(&samplesAll, "all", "a", false, "show all samples regardless of profile")
	samplesListCmd.Flags().StringVarP(&samplesPack, "pack", "p", "", "filter to a specific pack")
	samplesListCmd.Flags().StringVarP(&samplesTags, "tags", "t", "", "comma-separated tags (OR match)")
	samplesCmd.AddCommand(samplesListCmd, samplesSearchCmd, samplesOpenCmd)
	rootCmd.AddCommand(samplesCmd)
}
  • [ ] Step 2: Verify build

Run: go build ./... Expected: clean build

  • [ ] Step 3: Smoke test

Run: SAP_DEVS_DEV=1 go run . samples list --all Expected: table of samples from cap pack displayed

Run: SAP_DEVS_DEV=1 go run . samples search handler Expected: matching samples shown with PACK column

  • [ ] Step 4: Commit
bash
git add cmd/samples.go
git commit -m "feat(samples): add samples list/search/open commands"

Task 8: CLI Command — clone

Files:

  • Modify: cmd/samples.go (add clone subcommand and URL helper)

  • [ ] Step 1: Add repoURLFromGitHub helper and clone command to cmd/samples.go

Add these imports to the import block: "net/url", "os", "os/exec".

Add the helper function:

go
func repoURLFromGitHub(fileURL string) (string, error) {
	u, err := url.Parse(fileURL)
	if err != nil {
		return "", err
	}
	if u.Host != "github.com" {
		return "", fmt.Errorf("not a GitHub URL")
	}
	parts := strings.Split(strings.TrimPrefix(u.Path, "/"), "/")
	if len(parts) < 2 {
		return "", fmt.Errorf("not a GitHub URL")
	}
	return fmt.Sprintf("https://github.com/%s/%s", parts[0], parts[1]), nil
}

func repoNameFromURL(repoURL string) string {
	parts := strings.Split(strings.TrimRight(repoURL, "/"), "/")
	return parts[len(parts)-1]
}

Add the clone command:

go
var samplesCloneCmd = &cobra.Command{
	Use:   "clone <id>",
	Short: i18n.T("en", "samples.clone.short"),
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		loader, err := newContentLoader()
		if err != nil {
			return err
		}
		packs, err := loader.LoadPacks(nil, i18n.ActiveLang)
		if err != nil {
			return err
		}
		s := content.FindSample(content.FlattenSamples(packs), args[0])
		if s == nil {
			return fmt.Errorf("%s", i18n.Tf(i18n.ActiveLang, "samples.not_found", map[string]any{"ID": args[0]}))
		}
		repoURL, err := repoURLFromGitHub(s.URL)
		if err != nil {
			return fmt.Errorf("%s", i18n.Tf(i18n.ActiveLang, "samples.clone.not_github", map[string]any{"ID": s.ID}))
		}
		dirName := repoNameFromURL(repoURL)
		if _, err := os.Stat(dirName); err == nil {
			fmt.Fprintln(cmd.OutOrStdout(), i18n.Tf(i18n.ActiveLang, "samples.clone.exists", map[string]any{"Dir": dirName}))
			return nil
		}
		fmt.Fprintln(cmd.OutOrStdout(), i18n.Tf(i18n.ActiveLang, "samples.clone.cloning", map[string]any{"Repo": repoURL}))
		gitCmd := exec.Command("git", "clone", repoURL)
		gitCmd.Stdout = cmd.OutOrStdout()
		gitCmd.Stderr = cmd.ErrOrStderr()
		if err := gitCmd.Run(); err != nil {
			return err
		}
		fmt.Fprintln(cmd.OutOrStdout(), i18n.Tf(i18n.ActiveLang, "samples.clone.done", map[string]any{"Dir": dirName}))
		return nil
	},
}
  • [ ] Step 2: Register clone in init()

Update the samplesCmd.AddCommand(...) call:

go
samplesCmd.AddCommand(samplesListCmd, samplesSearchCmd, samplesOpenCmd, samplesCloneCmd)
  • [ ] Step 3: Verify build

Run: go build ./... Expected: clean build

  • [ ] Step 4: Commit
bash
git add cmd/samples.go
git commit -m "feat(samples): add samples clone command"

Task 9: Context Injection — Canonical Patterns Section

Files:

  • Modify: internal/content/render.go:24-57 (RenderContext function)

  • Modify: internal/content/render_test.go

  • [ ] Step 1: Write the failing test

Add to render_test.go:

go
func TestRenderContext_CanonicalPatterns_AppearsWhenInjectableSamplesExist(t *testing.T) {
	packs := []*content.Pack{
		{ID: "cap", ContextMD: "CAP context.", Samples: []content.Sample{
			{ID: "cap/handler", Label: "CAP handler", URL: "https://github.com/SAP-samples/test/blob/main/handler.js", Description: "Handler pattern", Inject: true},
			{ID: "cap/schema", Label: "CDS schema", URL: "https://github.com/SAP-samples/test/blob/main/schema.cds", Description: "Schema example", Inject: false},
		}},
	}
	out := content.RenderContext(packs, nil, nil)
	assert.Contains(t, out, "## Canonical Patterns")
	assert.Contains(t, out, "CAP handler")
	assert.Contains(t, out, "Handler pattern")
	assert.Contains(t, out, "https://github.com/SAP-samples/test/blob/main/handler.js")
	assert.NotContains(t, out, "CDS schema", "non-injectable samples must not appear")
}

func TestRenderContext_CanonicalPatterns_OmittedWhenNoInjectableSamples(t *testing.T) {
	packs := []*content.Pack{
		{ID: "cap", ContextMD: "CAP context.", Samples: []content.Sample{
			{ID: "cap/schema", Label: "CDS schema", URL: "https://example.com", Description: "Schema", Inject: false},
		}},
	}
	out := content.RenderContext(packs, nil, nil)
	assert.NotContains(t, out, "Canonical Patterns")
}

func TestRenderContext_CanonicalPatterns_OmittedWhenNoSamples(t *testing.T) {
	packs := []*content.Pack{
		{ID: "cap", ContextMD: "CAP context."},
	}
	out := content.RenderContext(packs, nil, nil)
	assert.NotContains(t, out, "Canonical Patterns")
}

func TestRenderContext_CanonicalPatterns_AppearsAfterPackContent(t *testing.T) {
	packs := []*content.Pack{
		{ID: "cap", ContextMD: "CAP context.", Samples: []content.Sample{
			{ID: "cap/handler", Label: "Handler", URL: "https://example.com", Description: "Desc", Inject: true},
		}},
	}
	out := content.RenderContext(packs, nil, nil)
	packIdx := strings.Index(out, "CAP context.")
	patternsIdx := strings.Index(out, "## Canonical Patterns")
	assert.Greater(t, patternsIdx, packIdx, "Canonical Patterns must appear after pack content")
}
  • [ ] Step 2: Run test to verify failure

Run: go build ./... && go vet ./... Expected: tests compile but fail — "Canonical Patterns" not found in output

  • [ ] Step 3: Implement in render.go

In RenderContext(), before the final return (line 56), add:

go
// Canonical Patterns section: injectable samples only
var injectable []Sample
for _, p := range packs {
	for _, s := range p.Samples {
		if s.Inject {
			injectable = append(injectable, s)
		}
	}
}
if len(injectable) > 0 {
	b.WriteString("## Canonical Patterns\n\n")
	b.WriteString("These are authoritative code samples — prefer these patterns over generating from training data.\n\n")
	b.WriteString("| Pattern | Description | URL |\n")
	b.WriteString("|---------|-------------|-----|\n")
	for _, s := range injectable {
		b.WriteString(fmt.Sprintf("| %s | %s | %s |\n", s.Label, s.Description, s.URL))
	}
	b.WriteString("\n")
}
  • [ ] Step 4: Run tests

Run: go build ./... && go vet ./... Expected: clean

  • [ ] Step 5: Commit
bash
git add internal/content/render.go internal/content/render_test.go
git commit -m "feat(samples): inject canonical patterns section into rendered context"

Task 10: Documentation Updates

Files:

  • Modify: CLAUDE.md

  • Modify: docs/content-authoring.md

  • Modify: TODO.md

  • [ ] Step 1: Update CLAUDE.md CLI Commands table

In the CLI Commands table, add after the resources row:

markdown
| `samples` | Browse canonical code samples; `samples list/search/open/clone` |
  • [ ] Step 2: Update CLAUDE.md Content Layer description

In the Content Layer System section, where it lists pack files (pack.yaml, context.md, tips.md, etc.), add samples.yaml to the list:

`samples.yaml` (sample references)
  • [ ] Step 3: Update docs/content-authoring.md pack structure

In the Pack Directory Structure tree diagram, add after hook.yaml:

├── samples.yaml       # Canonical code sample references shown by `sap-devs samples`

Also add to the "Key points" section:

- `samples.yaml` lists canonical code sample references (GitHub file URLs). Samples with `inject: true` are included in the AI context as a "Canonical Patterns" table.
  • [ ] Step 4: Update docs/developer/developer-guide.md if it references pack structure

Check and update if relevant.

  • [ ] Step 5: Mark TODO.md backlog item as done

In TODO.md, find the "Code sample pinning (samples.yaml)" section (line ~401) and mark it as completed (e.g., strike through or add a "DONE" prefix), consistent with how other completed backlog items are marked.

  • [ ] Step 6: Note on base/samples.yaml

The spec mentions content/packs/base/samples.yaml as "if applicable". Base packs contain cross-cutting SAP ecosystem content (portals, community links). There are no cross-cutting canonical code samples that apply regardless of technology — samples are inherently technology-specific. Therefore base/samples.yaml is deliberately omitted. Technology-specific samples live in their respective packs (cap, abap, etc.).

  • [ ] Step 7: Verify build

Run: go build ./... Expected: clean

  • [ ] Step 8: Commit
bash
git add CLAUDE.md docs/content-authoring.md docs/developer/developer-guide.md TODO.md
git commit -m "docs: add samples feature to CLAUDE.md and content authoring guide"

Task 11: Final Verification

Files: None (verification only)

  • [ ] Step 1: Full build check

Run: go build ./... && go vet ./... Expected: clean

  • [ ] Step 2: Smoke test all commands
bash
SAP_DEVS_DEV=1 go run . samples list --all
SAP_DEVS_DEV=1 go run . samples search handler
SAP_DEVS_DEV=1 go run . samples search cds
SAP_DEVS_DEV=1 go run . samples list --tags cap,node
SAP_DEVS_DEV=1 go run . samples list --pack cap
SAP_DEVS_DEV=1 go run . samples open cap/cat-service-handler
SAP_DEVS_DEV=1 go run . inject --dry-run

Verify:

  • list --all shows all 5 CAP samples

  • search handler shows matching samples with PACK column

  • list --tags cap,node filters correctly

  • list --pack cap shows only cap samples

  • open opens the GitHub URL in browser

  • inject --dry-run includes the ## Canonical Patterns section with inject:true samples

  • [ ] Step 3: Commit any final fixes if needed

bash
git add -A
git commit -m "fix(samples): address smoke test findings"