This file guides Claude Code (claude.ai/code) working in this repo.
A tutorial hosting platform replacing Adobe Experience Manager (AEM) as the frontend for developers.sap.com. Fetches tutorial markdown from the sap-tutorials GitHub org at build time, parses it into Hugo static pages styled with SAP Fundamental Styles (Horizon theme), and deploys behind an AppRouter on SAP BTP Cloud Foundry with XSUAA auth. Backed by a CAP Node.js service with SAP HANA Cloud for progress tracking (IMS rewrite) and tutorial content persistence (HTML as gzip BLOBs, served dynamically from HANA — no static file fallback). AEM has been fully decommissioned. PROD cutover: end of July 2026.
Full list: jq '.scripts' package.json. Operationally important:
# Quick start
npm install && npm run setup && npm run fetch-tutorials && npm run dev
# Frontend / build
npm run setup # Fresh-worktree only: hugo-apps install + better-sqlite3 native rebuild
npm run fetch-tutorials # Required before dev/build; caches in .tutorial-cache/
npm run dev # Hugo dev server (http://localhost:1313)
npm run build:all # Full production build (fetch + CSS + apps + Hugo + display)
# CAP backend
cds watch # Local CAP (http://localhost:4004), in-memory SQLite
npm run dev:hybrid # CAP + approuter against real HANA (parallel)
npm run bind:setup # First-time hybrid env binding setup
npm run setup-dev-data # Populate slugs + clean autotest data (needs cds bind)
# Tests
npm test # Unit (in-memory SQLite, fast)
npm run test:hybrid # Hybrid (real HANA via cds bind --exec; requires cf login)
npm run test:smoke # Smoke (HTTP against deployed; set SMOKE_BASE_URL/SMOKE_SRV_URL)
npm run test:e2e # Admin-UI Playwright smoke (post-deploy; set SMOKE_BASE_URL + SMOKE_TECH_USER/PASSWORD; self-skips when absent)
# Content publish (canonical: gh workflow; workstation is emergency-only)
gh workflow run rebuild-content.yml --repo sap-tutorials/tutorials-ims --ref main \
-f slug=<slug> # one-tutorial hotfix, ~2 min (mode auto-inferred)
gh workflow run rebuild-content.yml --repo sap-tutorials/tutorials-ims --ref main \
-f mode=full # full rebuild, ~10 min
# Migration & QA channel — see docs/developers/operations/{migration-from-ims,qa-channel-bootstrap}.mdFetch tutorials before dev or build. Delete .tutorial-cache/ to force re-fetch.
Runbook: content-rollback.md. Default mode is correctness-equivalent to --force (server carries forward unchanged slugs). Auto-verifies after publish; exits 2 on hash mismatch. Mutually exclusive flags: --force (skip hash round-trip), --verify-only, --heal. Every publish records initiator on ContentManifest.initiator for attribution.
sap-tutorials GitHub repos
→ scripts/fetch-tutorials.ts (fetch + cache markdown, --target hugo)
→ scripts/parsers/ (frontmatter, steps, images, options)
→ hugo/content/tutorials/*.md → Hugo build → hugo/public/tutorials/*/index.html
→ scripts/publish-content.ts (delta publish to HANA BLOBs via /content/publish)
CAP backend (http://localhost:4004 or CAP_BASE_URL)
→ GET /build/catalog → missions/paths → hugo/content/missions/*.md and groups/*.md
Tutorial HTML is not served from static files. publish-content.ts uploads gzip BLOBs to HANA via POST /content/publish; AppRouter routes /tutorials/* to CAP /content/tutorials/:slug. Content pages (/browse/, /topics/, verbs, sitemaps, etc.) use the same HANA BLOB pattern under the page-<name> key namespace — non-homepage routes are now flipped to CAP /content/pages/* (Phase 2). Homepage / flip is deferred (separate follow-up PR, flipped last after DEV cache/fail-open verification). See docs/developers/architecture/build.md §"Content Pages from HANA".
Deep dives (do not duplicate here — read the doc when relevant):
- docs/developers/architecture/build.md — content pipeline, parsers, testing workspaces
- docs/developers/architecture/authentication.md — auth flow, XSUAA, data privacy
- docs/developers/architecture/homepage.md — developer-portal homepage
- docs/developers/architecture/advocates.md — Developer Advocates page
- docs/developers/architecture/observability.md — metrics module, snapshots
- docs/developers/architecture/cross-container-integration.md — reusable HDI↔HDI cross-container playbook (versioned views, grants/synonyms,
@cds.persistence.existsfacades, bootstrap sequencing) - docs/developers/operations/testing-endpoints.md — canonical UI + API endpoint reference (services, custom endpoints, auth scopes)
- docs/developers/operations/mta-deployment.md — deploy runbook
- docs/developers/operations/rebuild-content-workflow.md — three rebuild modes, auto-classify
- docs/developers/operations/qa-channel-bootstrap.md — QA author-preview channel
- docs/developers/operations/scheduler-troubleshooting.md — outbox-wedge recovery runbook
- docs/developers/reference/tutorials-ims-gotchas.md — everything else (build pipeline quirks, publish flags, AI features, env vars, migration)
- docs/developers/reference/generative-ui-research.md — research spike (#2362): MCP Apps + json-render evaluation, ranked use cases, json-render challenge-widget POC
Subsystem one-liners:
- CAP srv/ — 12 services under
@pathprefixes (see testing-endpoints.md). Content persistence insrv/lib/content-store.js. WebSocket via@cap-js-community/websocket(Socket.IO) on/ws/display+/ws/event-stream. Jobs insrv/jobs/(scheduler.js) — scheduled via CAP 10's Scheduling API through the internalCronServiceinsrv/cron-service.js(#958). - Admin UI (
app/admin-shell/+app/admin/) — 14 Fiori Elements components loaded as headless componentUsages inside a unifiedsap.tnt.ToolPageshell. Served at/admin-ui/(XSUAA-protected). Theme:sap_horizonwith auto-detect. - Analytics Explorer (
app/analytics-explorer/) — Vue 3 SPA at/analytics-ui/. Ad-hoc SQL overAnalyticsService; SELECT-only allowlisted viasrv/lib/analytics-sql-validator.cjs. - Scanner (
app/scanner/) — UI5 barcode scanner at/scanner-ui/. Usessap.ndc.BarcodeScanner. - Hugo + Vue islands —
hugo/= static site,hugo-apps/= ~17 Vue 3 islands intohugo/static/js/(not routes). Standalone Vue appapp/display-app/for event monitors.
-
Canonical local deploy (CI is bypassed for most ad-hoc deploys):
# Point CAP_BASE_URL at the DEPLOYED backend so CAP-sourced pages (/concepts/, # advocates, homepage shelves) bake with real content. build:deploy fails fast # if CAP_BASE_URL is unset/localhost — guards the 2026-07-12 empty-concepts incident. export CAP_BASE_URL="https://tutorial-system-dev-tutorials-srv.cfapps.eu10-005.hana.ondemand.com" npm run build:deploy # Hugo MUST finish before mbt build cd .deploy && mbt build && cf deploy mta_archives/*.mtar -e ../deploy/dev.mtaext -f
mbt buildonlycp'shugo/public/into the approuter — it does not run Hugo orfetch-tutorials. Local builds MUST runnpm run build:allbeforembt build; skipping ships stale approuter (missing NEW badges, license icons, progress UI). Always confirm deploy scope with maintainer (backend-only / +content / +QA). -
The MCP server is a SEPARATE MTA (
tutorials-mcp,mta-mcp.yaml) — the command above deploys only the maintutorials-imsMTA (.deploy/mta.yaml), which serves the anonymous/mcp/*, PAT/mcp-pat/*, and admin/mcp-admin/*surfaces. The authenticated/mcp-auth/*surface runs in thetutorials-srv-mcpapp of the separatetutorials-mcpMTA.npm run deploy/scripts/deploy-mta.cjsdo NOT cover it — deploy it by hand from repo root:mbt build -f mta-mcp.yaml -t mta_archives cf deploy mta_archives/*.mtar -e deploy/mcp-dev.mtaext -f # or deploy/mcp-prod.mtaext
The MCP MTA adopts the main MTA's services (hana/xsuaa/identity/credstore/destination) as
existing-service, so deploy the main MTA first on a fresh space. Feature flagMCP_AUTH_ENABLEDtargetstutorials-srv-mcp;MCP_PAT_MINT_ENABLED/KG_STEP_SLICER_ENABLEDtargettutorials-srv. See mta-deployment.md § MCP MTA. -
Local deploy is envsubst-free — All four secrets (
CONTENT_API_KEY,REBUILD_API_KEY,APPROUTER_URL,GITHUB_DISPATCH_TOKEN) formerly injected viaenvsubstnow live exclusively in the BTP Credential Store (or have been removed entirely, in APPROUTER_URL's case). Runcf deploy mta_archives/*.mtar -e ../deploy/dev.mtaext -fdirectly. Rotation happens through/admin-ui/#secretson the target env's approuter. See mta-deployment.md "Local deploy no longer needs envsubst" for the full context. -
PR over direct merge — Default to
gh pr createfrom a feature branch; subagent code review is not a substitute for PR review. Only fast-merge tomainif explicitly told to skip the PR. -
srv-qacp list audit — When changing anything undersrv/lib/, re-walk transitive./imports fromsrv/lib/content-store.jsand confirm every dep is in.deploy/mta.yaml'ssrv-qacplist. Missing transitive deps crash QA boot at MTA deploy time. -
Admin-UI changes need a FULL deploy + are bundle-gated — the admin apps (
app/admin/*+app/admin-shell/) are raw-copied into the approuter'sstatic/admin-ui/by the MTA's approuter builder duringmbt build. A--skip-builddeploy (reuses a stale mtar), a module-scopedcf deploy -m tutorials-srv, or an mtar packaged before the change landed will silently ship a stale admin UI even though the fix is onmain(this is why PR #1331/#1345's value-help fix looked "not deployed" on DEV).npm run deploynow runs Step 3.5 (scripts/check-shipped-admin-bundle.cjs) which cracks the mtar and diffs the shipped admin component files against source, failing the deploy on drift. Rule: deploy admin-UI changes with a fullnpm run deploy -- --env <env>(NO--skip-build, NO-mscoping), and never bypass Step 3.5.
These are load-bearing habits for keeping AI-agent token usage down without reducing output. Ignoring them wastes tokens re-sending output the agent never uses.
- Never paste raw deploy/test/log scrollback into the main context. Any command whose output can exceed ~50 lines (
mbt build,cf deploy,npm run build:all,npm test,cf logs) MUST be wrapped inscripts/quiet-run.sh <cmd…>— it keeps the full log on disk under.quiet-logs/and prints only exit code + matched error lines + last N lines. Drill into the full log path only when the summary is insufficient. - Delegate noisy investigation to a subagent. Grep sweeps, "which files import X", log trawls, broad multi-file reads — dispatch to
Explore/general-purposeso the raw output stays out of the main context and only the findings return. - Don't read a whole file for a few lines. Prefer cds-mcp / hana-cli /
Grepwith line ranges over full-fileRead; read only the slice you need. - Short sessions. One deploy or one debugging thread per session;
/clearbetween them so stale build output doesn't ride on every subsequent request. - Longer term, structural wins are tracked in issues #2265 (retrieval memory), #2266 (deploy-oracle), #2267 (verify/probe), #2268 (CDS answer path).
The load-bearing few. Full detail for every relocated item → tutorials-ims-gotchas.md ("Top Gotchas — full detail" section); items with their own reference doc link straight to it.
- Fresh worktree: run
npm run setupafternpm install— global npmrcignore-scripts=trueskips native builds; without ithugo-apps/node_modulesis empty andbetter-sqlite3won't build (tests hang / fail resolving@mediapipe/tasks-vision). ignore-scripts=truesilences allpre*/post*hooks — localbuild:alldoes NOT firepostbuild:apps; island-manifest +build:page-fallbackare wired as explicitbuild:allsteps. Rely on the hook and merged JS/CSS ships dead (unhashed island paths) → "not deployed" despite a green deploy. → gotchas.md "Build artifacts & lifecycle hooks".hugo/content/tutorials/is entirely generated — never edit;fetch-tutorialsoverwrites. Editscripts/parsers/or source repos.- Group/Mission completions are rollup-derived (#1934) —
srv/lib/completion-rollup.jsrecomputes parents after any TUTORIAL/PUZZLE/CHECKPOINT/PETOBERFEST completion; upserts on(user_ID, taskLegacyId, taskType); NOT acontent-store.jsdep. → gotchas.md "Completions rollup". - Never run
publish-contentfrom a workstation — usegh workflow run rebuild-content.yml; workstation publishes skip CI validation. - Tutorial slugs are lowercase canonical — always
.toLowerCase()before comparing to publish payload; mismatch = "0 steps" on group SSR. TutorialMeta+Tutorials/Missions/Groupsslugs are unique — new write paths MUST upsert on slug (SELECT-then-UPDATE-or-INSERT); pattern atsrv/lib/content-publish-session.js:285/:349.- Never SELECT a HANA BLOB alongside metadata in one CDS QL query — LOB locators expire; use raw
db.run()(srv/lib/content-store.js,srv/lib/embedding-query.js). CDS QL is fine on SQLite unit tests. CONTENT_API_KEYenv var required forPOST /content/publish+/content/rollback(missing → 401).- Content served from mutable
ContentCurrent(Option B, #2017) — 3 env flags gate it (all default OFF, flip in order); rollback replaysContentHistory. Serve headerX-Content-Source: db-currentvsdb. → gotchas.md "Content model — mutable ContentCurrent". - GitHub Actions secret is
DISPATCH_TOKEN(GH reservesGITHUB_) — runtime varGITHUB_DISPATCH_TOKEN, read bysrv/lib/rebuild-trigger.js. rebuild-content.ymlauto-infersmode=slug-targetedwhenslugis set — don't pass mode. Wall-clock: catalog ~5m, slug ~2m, full ~10m.- Alert saves do NOT trigger rebuilds — runtime-served; cache-bust on save only, up-to-60s delay.
- NGDS auto-send is PROD-only + DB-gated (double gate) — fires only when CF
space_name==='prod'ANDImsConfig ngds.autosend.enabled==='true'; edge-only, fails closed, never throws into the completion tx, allowlisted to TUTORIAL/GROUP/MISSION. → gotchas.md "NGDS auto-send". @cap-js/aifor RPT-1 ValueList recommendations (#959) —AICore-mockedlocally,AICore-btpin hybrid/prod; per-field opt-out@UI.RecommendationState: 0. Ref: cap-ai-plugin.md.- Knowledge-graph feature flags (
KG_*), all default OFF + DEV-only, all fail-open — PageRank #916, WCC isolation #918, on-demand extraction #948, Louvain communities #917, orphan retirement #1115, community peers/labels #1126, community search weight #1171, coverage nudge #1172, cluster Q&A #1173. Toggles, nightly jobs, and fail-open specifics → gotchas.md "Knowledge graph feature flags". - HCQL protocol adapter (#2247, CAP 10.1.0) —
POST /hcql/<svc>paths (NOT co-mounted on OData URLs) on 5 authenticated services only (AdminService,AuthorService,AnalyticsService,ExportsService,ConsolidationService); requires@sap/cds >= 10.1.0. Malformed-CQN process-exit DoS from 10.0.3 fixed in 10.1.0 (now returns 400, server stays up). Auth enforced before CQN parse. Kill: deletesrv/hcql-enablement.cds+ drop thehcqlentry fromAdminService's@protocollist insrv/admin-service-mcp.cds, thencds build --production+ redeploy. Ref: hcql-support.md. - cds-caching CDS-DB store + metrics ON (#1222) —
store:"cds"in hybrid/prod,memoryin base/unit. CF resolve-guard needs BOTH the baked csn entities ANDsrv/lib/strip-precompiled-plugin-roots.js(both load-bearing). Ref: cds-caching-store.md. - User-facing UI changes want a committed e2e spec — advisory PR nudge on
app/**/hugo/**changes; real coverage runs in the post-DEV-deploye2ejob. Ref: e2e-coverage-pattern.md. test:e2eis post-deploy only, not on PRs — self-skips withoutSMOKE_BASE_URL. Served tutorials render<main>+<h1>, NOT<article>. Runbook:test/e2e/README.md.- Freshness detector grounding needs the corpus-embedding backfill — until
srv/jobs/freshness-corpus-embedding-job.jsruns, every API-obsolescence claim degrades toconfidence: Low. Tutorial source fromContentFiles.sourceContentviagetTutorialSource(slug), NOTSteps.description. → gotchas.md "Freshness detector". - External channels subsystem —
Channelsentity (db/channels.cds) is the source of truth; re-ingest vianpm run seed-channels; directory at/channels(baked byfetch-channelsinbuild:all); verb-lane fill vianpm run promote-channels; community items never land inSTART_HERE. → channels.md. - TechEd sessions subsystem (#2312) — new
TechEd*entities (db/external/teched.cds, NOT reusedCommunityEvents), loaded via ausingside-effect indb/external-content.cds(don't delete the import); RainFocus ingest (POST https://events.rainfocus.com/api/sessions, headersrfapiprofileid/rfwidgetidper venue fromRAINFOCUS_{TE26,TEV26}_PROFILE/_WIDGETenv — live-confirmed defaults baked in, IDs may rotate; speakers embedded inparticipants[], tracks inattributevalues[attribute=="Track"], offset-paginated); idempotent seed viascripts/seed-teched.cjs; public/build/techedfeed; corrected fetcher + delta cron jobs shipped in #2332;/teched/page + island, KG projection, embeddings, and Devtoberfest cross-linking are later units. → teched.md. - Signed provenance envelope (#2245) —
PROVENANCE_ENVELOPE_ENABLED(DB config, default OFF, DEV-first); two anonymous Express endpoints (/content/tutorials/:slug/provenanceand/.well-known/tutorial-provenance/jwks.json); signing key =PROVENANCE_SIGNING_KEYcredstore secret (Ed25519 PKCS8 PEM); rotate via/admin-ui/#secrets. Fail-open. → gotchas.md "Signed provenance envelope".