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) andinternal/content/pack.go:266-280(LoadPack YAML block)[ ] Step 1: Add the Sample struct to pack.go
After the Influencer struct (line 108), add:
// 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:
Samples []Sample- [ ] Step 3: Add samples.yaml loading to LoadPack
In LoadPack(), after the influencers.yaml loading block (lines 248-253), add:
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
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:
// 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:
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:
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:
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
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.goCreate:
internal/content/samples_test.go[ ] Step 1: Write samples_test.go with all tests
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
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
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.jsonModify:
.vscode/settings.json[ ] Step 1: Create samples.schema.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:
"./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
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
- 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
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.jsonModify:
internal/i18n/catalogs/de.json[ ] Step 1: Add English strings to en.json
After the influencers block (after line 200), add:
"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:
"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
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.goNote:
cmd/root.gois NOT modified — cobra'sinit()pattern withrootCmd.AddCommand(samplesCmd)incmd/samples.gohandles registration automatically, same as all other command files.[ ] Step 1: Create cmd/samples.go with list, search, and open
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
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:
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:
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:
samplesCmd.AddCommand(samplesListCmd, samplesSearchCmd, samplesOpenCmd, samplesCloneCmd)- [ ] Step 3: Verify build
Run: go build ./... Expected: clean build
- [ ] Step 4: Commit
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:
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:
// 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
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.mdModify:
docs/content-authoring.mdModify:
TODO.md[ ] Step 1: Update CLAUDE.md CLI Commands table
In the CLI Commands table, add after the resources row:
| `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
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
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-runVerify:
list --allshows all 5 CAP samplessearch handlershows matching samples with PACK columnlist --tags cap,nodefilters correctlylist --pack capshows only cap samplesopenopens the GitHub URL in browserinject --dry-runincludes the## Canonical Patternssection with inject:true samples[ ] Step 3: Commit any final fixes if needed
git add -A
git commit -m "fix(samples): address smoke test findings"