Skip to content

Learn Command Design ​

Date: 2026-04-18 Status: Approved Approach: Full Internal Package (Approach B)

Overview ​

sap-devs learn is an umbrella command that aggregates content from three existing sources — learning journeys (learning.sap.com), tutorials (developers.sap.com), and Discovery Center missions — into a unified learning recommendation experience. It provides profile-filtered, experience-level-aware recommendations and curated learning paths.

Subcommands ​

sap-devs learn                          → defaults to recommend
sap-devs learn recommend                → sectioned output (Journeys / Tutorials / Missions)
sap-devs learn search <query>           → cross-type search, unified table
sap-devs learn path                     → defaults to path list
sap-devs learn path list                → show available paths (curated + auto-filled)
sap-devs learn path show <path-id>      → show steps of a specific path (glamour-rendered)
sap-devs learn path open <path-id>      → open first URL in browser

Shared Flags (PersistentFlags on parent) ​

FlagTypeDefaultDescription
--levelstringconfig experience_levelFilter by level (beginner, intermediate, advanced)
--allboolfalseBypass profile filtering
--count / -nint10Max items per section (recommend) or total (search)

Subcommand-specific Flags ​

FlagCommandTypeDescription
--packrecommend, path liststringFilter to a specific pack's content

Data Model ​

internal/learn/types.go ​

go
type ItemType string

const (
    ItemJourney  ItemType = "journey"
    ItemTutorial ItemType = "tutorial"
    ItemMission  ItemType = "mission"
)

type LearnItem struct {
    Type     ItemType
    Title    string
    Slug     string   // unique ID (slug for journeys/tutorials, stringified ID for missions)
    Level    string   // normalized lowercase: "beginner", "intermediate", "advanced"
    Duration string   // human-readable: "4 hr", "45 min", ""
    URL      string
    Featured bool
    PackID   string
    Product  string
}

type Recommendations struct {
    Journeys  []LearnItem
    Tutorials []LearnItem
    Missions  []LearnItem
}

type RecommendOptions struct {
    Level  string
    PackID string
    All    bool
    Limit  int
}

type LearningPath struct {
    ID          string
    Name        string
    Description string
    Level       string
    PackID      string
    Steps       []PathStep
    Generated   bool // true = auto-filled, false = curated
}

type PathStep struct {
    Type ItemType
    Slug string
    Item *LearnItem // resolved at runtime
}

Pack YAML: paths.yaml ​

New file alongside tutorials.yaml, learning.yaml, discovery.yaml:

yaml
paths:
  - id: cap-getting-started
    name: Getting Started with CAP
    level: beginner
    description: Build your first CAP application from scratch
    steps:
      - type: journey
        slug: developing-with-sap-cloud-application-programming-model
      - type: tutorial
        slug: cap-getting-started
      - type: tutorial
        slug: hana-cloud-cap-create-project
      - type: mission
        slug: "4327"

Pack struct addition ​

go
// In content.Pack:
LearningPaths []LearningPathDef // raw YAML shape, before resolution
go
// content.LearningPathDef is the YAML-parsed shape
type LearningPathDef struct {
    ID          string           `yaml:"id"`
    Name        string           `yaml:"name"`
    Description string           `yaml:"description,omitempty"`
    Level       string           `yaml:"level,omitempty"`
    Steps       []LearningPathStepDef `yaml:"steps"`
    PackID      string           // set at load time
}

type LearningPathStepDef struct {
    Type string `yaml:"type"` // "journey", "tutorial", "mission"
    Slug string `yaml:"slug"`
}

Package Structure: internal/learn/ ​

types.go ​

Types listed above.

recommend.go ​

go
func Recommend(
    journeys []learning.LearningJourney,
    tutorials []tutorials.TutorialMeta,
    missions []discovery.Mission,
    packs []*content.Pack,
    opts RecommendOptions,
) *Recommendations

Each content type runs its own three-tier resolution:

  1. Featured items from pack refs
  2. Non-featured pack refs
  3. Profile-filtered items from the full index (or all if opts.All)

Results are normalized into LearnItem and filtered by opts.Level if set. Each section is capped at opts.Limit.

Level normalization:

  • Learning journeys: "BEGINNER" → "beginner" (API returns uppercase)
  • Tutorials: already lowercase
  • Missions: effort "0"/"1" → "beginner", "2" → "intermediate", "3" → "advanced"

search.go ​

go
func Search(
    journeys []learning.LearningJourney,
    tutorials []tutorials.TutorialMeta,
    missions []discovery.Mission,
    query string,
    opts RecommendOptions,
) []LearnItem

Searches all three indexes locally using case-insensitive substring match on title and description. Results are interleaved by relevance (match in title ranks higher than match in description). Level filtering applied if set. Capped at opts.Limit.

paths.go ​

go
func LoadPaths(packs []*content.Pack) []LearningPath

func AutoFillPaths(
    packs []*content.Pack,
    journeys []learning.LearningJourney,
    tutorials []tutorials.TutorialMeta,
    missions []discovery.Mission,
) []LearningPath

func ResolvePaths(
    paths []LearningPath,
    journeys []learning.LearningJourney,
    tutorials []tutorials.TutorialMeta,
    missions []discovery.Mission,
) []LearningPath

LoadPaths: reads LearningPaths from packs, converts LearningPathDef → LearningPath (unresolved).

AutoFillPaths: for packs without paths.yaml, collects featured tutorials/journeys/missions, groups by level, generates synthetic paths named "{Pack Name} — {Level}". Only generates a path if ≥2 items exist at that level. Sets Generated = true.

ResolvePaths: populates each PathStep.Item by looking up the slug in the respective index. Steps whose items are no longer in the index get Item = nil (rendered with a "(not found)" marker).

Config Changes ​

Add ExperienceLevel to internal/config/config.go:

go
type Config struct {
    // ... existing fields ...
    ExperienceLevel string `yaml:"experience_level,omitempty"`
}

Valid values: "beginner", "intermediate", "advanced", or "" (no filtering). Set via sap-devs config set experience_level beginner. The learn commands read this as the default --level value; the --level flag overrides it.

Content Loader Changes ​

In LoadPack() (internal/content/pack.go), add alongside existing YAML loading:

go
if data, err := os.ReadFile(filepath.Join(packDir, "paths.yaml")); err == nil {
    var pathsYAML struct {
        Paths []LearningPathDef `yaml:"paths"`
    }
    _ = yaml.Unmarshal(data, &pathsYAML)
    pack.LearningPaths = pathsYAML.Paths
    for i := range pack.LearningPaths {
        pack.LearningPaths[i].PackID = pack.ID
    }
}

Content flattening helper in internal/content/:

go
func FlattenLearningPaths(packs []*Pack) []LearningPathDef

Command Layer ​

cmd/learn.go ​

Parent command + recommend subcommand. Pattern matches cmd/learning.go:

  • Uses newContentLoader() and config.LoadProfile() from root.go
  • Loads packs via loader.LoadPacks(profile, lang)
  • Loads indexes:
    • learning.LoadIndex(cacheDir, learning.CacheTTL) → ([]LearningJourney, bool)
    • tutorials.LoadIndex(cacheDir) → ([]TutorialMeta, error) — treat non-nil error as "not cached"
    • discovery.LoadCache[[]discovery.Mission](cacheDir, "missions", discovery.CacheTTL) → ([]Mission, bool)
  • Missing indexes are tolerated (section skipped with hint message)
  • If all three indexes missing: error "No content cached — run sap-devs sync first"
  • Calls learn.Recommend(), renders three tabwriter sections

cmd/learn_search.go ​

Cross-type search. Loads all three indexes, calls learn.Search(), renders unified table with TYPE column.

cmd/learn_path.go ​

path parent (defaults to path list) + list, show, open subcommands.

  • list: loads curated paths + auto-fills, renders table (NAME, LEVEL, STEPS, PACK, SOURCE)
  • show &lt;path-id&gt;: resolves path, renders numbered step list with glamour markdown
  • open &lt;path-id&gt;: opens first step's URL in browser via pkg/browser

Registration (init) ​

go
func init() {
    learnCmd.PersistentFlags().StringVar(&learnLevel, "level", "", "...")
    learnCmd.PersistentFlags().BoolVar(&learnAll, "all", false, "...")
    learnCmd.PersistentFlags().IntVarP(&learnCount, "count", "n", 10, "...")

    learnRecommendCmd.Flags().StringVar(&learnPack, "pack", "", "...")
    learnPathListCmd.Flags().StringVar(&learnPack, "pack", "", "...")

    learnPathCmd.AddCommand(learnPathListCmd, learnPathShowCmd, learnPathOpenCmd)
    learnCmd.AddCommand(learnRecommendCmd, learnSearchCmd, learnPathCmd)
    rootCmd.AddCommand(learnCmd)
}

Output Formats ​

recommend ​

Learning Journeys
  FEATURED  TITLE                                         LEVEL         DURATION
  ★         Developing with SAP CAP                       Beginner      4 hr
  ★         Becoming an SAP BTP Solution Architect        Intermediate  8 hr

Tutorials
  FEATURED  TITLE                                         LEVEL         TIME
  ★         Get Started Using SAP CAP                     Beginner      45 min
  ★         Deploy Your CAP App to Cloud Foundry          Intermediate  30 min

Discovery Center Missions
  FEATURED  TITLE                                         EFFORT
  ★         Develop a Full-Stack CAP Application          Easy
            GenAI Mail Insights with CAP and RAG          Medium

Sections with zero results omitted entirely.

  #  TYPE      TITLE                                      LEVEL         TIME
  1  Journey   Developing with SAP CAP                    Beginner      4 hr
  2  Tutorial  Get Started Using SAP CAP                  Beginner      45 min
  3  Mission   Develop a Full-Stack CAP Application       Easy          -

path list ​

  NAME                          LEVEL         STEPS  PACK    SOURCE
  Getting Started with CAP      Beginner      4      cap     curated
  SAP CAP — Intermediate        Intermediate  3      cap     auto

path show ​

Glamour-rendered markdown:

markdown
# Getting Started with CAP
**Level:** Beginner | **Pack:** cap | **Source:** curated

Build your first CAP application from scratch

1. [Journey] Developing with SAP CAP (4 hr)
   https://learning.sap.com/learning-journeys/developing-with-sap-cloud-application-programming-model

2. [Tutorial] Get Started Using SAP CAP (45 min)
   https://developers.sap.com/tutorials/cap-getting-started.html

3. [Tutorial] Create a CAP Project with HANA Cloud (30 min)
   https://developers.sap.com/tutorials/hana-cloud-cap-create-project.html

4. [Mission] Develop a Full-Stack CAP Application
   https://discovery-center.cloud.sap/missiondetail/4327/

i18n ​

Keys added to internal/i18n/catalogs/en.json and de.json:

  • learn.short, learn.long
  • learn.recommend.short, learn.search.short
  • learn.path.short, learn.path.list.short, learn.path.show.short, learn.path.open.short
  • learn.col_type, learn.col_title, learn.col_level, learn.col_duration
  • learn.col_name, learn.col_steps, learn.col_pack, learn.col_source
  • learn.section_journeys, learn.section_tutorials, learn.section_missions
  • learn.no_content, learn.path_not_found, learn.step_not_found

JSON Schema ​

New content/schemas/paths.yaml.schema.json for paths.yaml validation. Register in .vscode/settings.json.

Edge Cases ​

  • Missing individual indexes: gracefully degrade — show available sections, skip missing with hint
  • Empty paths.yaml: treated as no curated paths; auto-fill kicks in
  • Duplicate slugs across packs: ResolvePaths resolves against global indexes; same item may appear in multiple paths
  • --level with no experience_level config: no level filtering (show all levels)
  • Invalid --level value: validate in parent PersistentPreRunE; reject values outside {"beginner", "intermediate", "advanced", ""}
  • Unresolved path steps: shown with "(not found)" marker
  • All indexes missing: error with "run sap-devs sync first"

Testing ​

  • Unit tests in internal/learn/ for Recommend(), Search(), AutoFillPaths(), ResolvePaths() with fixture data
  • Local validation via go build ./... and go vet ./...
  • CI (ubuntu-latest) runs go test ./...

Files to Create/Modify ​

New files: ​

  • internal/learn/types.go
  • internal/learn/recommend.go
  • internal/learn/search.go
  • internal/learn/paths.go
  • internal/learn/recommend_test.go
  • internal/learn/search_test.go
  • internal/learn/paths_test.go
  • cmd/learn.go
  • cmd/learn_search.go
  • cmd/learn_path.go
  • content/packs/cap/paths.yaml (seed content)
  • content/schemas/paths.yaml.schema.json

Modified files: ​

  • internal/config/config.go — add ExperienceLevel field
  • internal/content/pack.go — add LearningPaths field, LearningPathDef type, load paths.yaml
  • internal/content/merge.go — merge LearningPaths in MergeWith() using LearningPathDef.ID as the matching key
  • internal/content/learning.go (or new internal/content/paths.go) — FlattenLearningPaths() helper
  • internal/i18n/catalogs/en.json — learn command strings
  • internal/i18n/catalogs/de.json — learn command strings (German)
  • .vscode/settings.json — register paths.yaml schema
  • CLAUDE.md — add learn command to CLI commands table and architecture section