tutorials-ims Gotchas β
Overflow for project-specific gotchas that used to live in CLAUDE.md. The top ~10 that repeatedly bite live in CLAUDE.md itself; everything else is here.
Cross-references:
- CAP/CDS gotchas β cap-cds-gotchas.md
- HANA / HDI gotchas β hana-hdi-gotchas.md
- Vue islands gotchas β vue-islands-gotchas.md
Build pipeline β
- POC tutorial list is dynamic β Tutorials are discovered from
sap-tutorialsGitHub org viadiscoverAllTutorials()inscripts/parsers/github.ts.EXCLUDED_REPOS(justtutorials-ims) skipped. Private repos excluded by default;INCLUDED_PRIVATE_REPOSis allowlist (currentlymeta-tutorials).-Contributionprivate repos gated byINCLUDE_CONTRIBUTION_REPOS/ONLY_CONTRIBUTION_REPOS. Discovery cached in.tutorial-cache/discovery-map.json.npm run discover-reposlists without fetching. - Validation quiz data from
-Contributionrepos βfetchRulesVr()inscripts/parsers/github.tsfetchesrules.vrfrom private-Contributionrepos. NeedsGITHUB_TOKEN. Cached at.tutorial-cache/<slug>.rules.vr. Parsed byscripts/parsers/rules.ts, injected into Hugo frontmatter steps. GITHUB_TOKENenv var βscripts/parsers/github.tsoptionally uses it to avoid GitHub API rate limits.CAP_BASE_URLenv var β Used byscripts/parsers/cap.tsand migration scripts. Defaults tohttp://localhost:4004.- Node.js >= 20 required β Build scripts use native
fetch(no polyfill). - Slug fields β
Missions.slugandCompletionPaths.slugmust be populated for the build pipeline to generate mission/group pages. Runnode scripts/migrate-reference-data.js populate-slugsafter data import.
Directory layout β
app/vshugo-apps/βapp/= standalone UI apps with their own builds (admin-shell,admin,analytics-explorer,scanner,display-app), each deploys by copyingdist//webapp/intoapprouter/static/<route>/.hugo-apps/= single Vite project compiling ~17 Vue 3 page-level islands intohugo/static/js/β loaded by Hugo templates as<script>tags, not deployed as routes.hugo-apps/src/{shared,composables}/are utility modules, not islands. See mta.yaml.- Vite β Hugo
js.Buildoutput collisions β Vite entries write tohugo/static/js/<name>.js. Hugo'sresources.Get "js/<X>.ts" | js.Buildwrites tohugo/public/js/<X>.jsafter Hugo copiesstatic/βpublic/, silently clobbering Vite if names collide.postbuild:appsrunstsx scripts/check-build-collisions.tsβ fix by renaming. /admin/is OData only β AdminService OData lives at/admin/. The admin shell UI is served at/admin-ui/to avoid path collisions.- Hugo vs VitePress β Project migrated from VitePress to Hugo.
site/.vitepress/still exists (with builtdist/) but is legacy. Active frontend work targetshugo/. hugo/content/tutorials/is entirely generated β Never edit these files directly; they're overwritten bynpm run fetch-tutorials. Editscripts/parsers/or source tutorials in thesap-tutorialsGitHub org.- Cache clearing β
.tutorial-cache/caches raw markdown, GitHub metadata, and CAP catalog data. Delete it to force a full re-fetch. No incremental invalidation.
Content persistence & publish β
- Tutorials are DB-only β HTML served exclusively from HANA BLOBs. No static file fallback. If nothing published,
/tutorials/*returns 404. - Content garbage collection β Daily cron (03:00) prunes
SUPERSEDED/ROLLED_BACKversions older than 7 days, keeping the 3 most recent for rollback. Never touchesACTIVE/PUBLISHING. publish-content.tsflags β Default mode is now correctness-equivalent to--force: server's commit carries forward unchanged slugs.--forceis a perf/CI-convenience flag (skips/content/hashesround-trip). CLI auto-verifies after publish; exits 2 on hash mismatch.--verify-only/--heal/--dry-run.--force/--heal/--verify-onlymutually exclusive.- HANA LOB locator expiry β CDS QL returns HANA BLOBs as
Readablestreams with locators that expire before consumption when mixed with non-BLOB columns.srv/lib/content-store.jsuses raw SQL (db.run()) for BLOB retrieval on HANA, CDS QL for SQLite tests. Never SELECT a BLOB alongside metadata in a single CDS QL query on HANA. - Tutorial embeddings live in
TutorialEmbeddingand are HANA-only at query time β SQLite test path uses JS-side cosine. Never SELECT theembeddingBLOB alongside metadata in a single CDS QL query on HANA; usedb.run()raw SQL insrv/lib/embedding-query.js. - Tutorial/Mission/Group slugs are unique (case-insensitive) β
@assert.unique.slugonTutorials,Missions,Groups. New write paths MUST upsert on slug, not blind-INSERT. Canonical pattern atsrv/lib/content-publish-session.js:285. Hybrid testtest/hybrid/duplicate-slugs.test.jsguards. Repair:npx cds bind --exec -- node scripts/merge-duplicate-slugs.cjs --commit. - TutorialMeta is a logical singleton (one row per tutorial) β
@assert.unique.tutorialonTutorialMeta. Auto-init atsrv/lib/content-publish-session.js:349checks existing before INSERT. Hybrid testtest/hybrid/duplicate-tutorial-meta.test.jsguards. Repair:npx cds bind --exec -- node scripts/dedupe-tutorial-meta.cjs --commit. MyTutorialsView.repositoryNamesources fromRepoCatalog.repo, NOTTutorialMeta.repository(#1063) β TheTutorialMeta.repository β TutorialRepositories.namechain is de-facto empty in DEV (0/2930 rows haverepository_IDset; publish flow never populates it, only the legacy backfill script does), andTutorialRepositoriesis missing rows for the flagshipTutorialsrepo entirely.RepoCatalogis populated on every content publish bysrv/lib/repo-catalog.jsand covers 100% of live tutorials β that's whatMyTutorialsView+MyMonitoredTutorialsViewnow left-join to.TutorialMeta.repositoryFK is retained for other consumers (scripts/soft-delete-sandbox-tutorials.cjspass-1) but is no longer the source of truth for the Sage-facing view. If you're adding a newrepositoryName-shaped field on any view, joinRepoCatalogonslug; do not chase the TutorialMeta chain.
QA channel β
- QA channel content β
/tutorials-qa/*is gated by XSUAA scopeTutorial.Author. Content sourced only from*-Contributionrepos viaONLY_CONTRIBUTION_REPOS=true. Lives intutorials-db-qaHDI; never queries prod tables. .tutorial-cache-qa/vs.tutorial-cache/β separate caches per channel.fetch-tutorialswrites a.channelmarker;devwarns if content channel doesn't match.CONTENT_API_KEY_QAenv var β required forPOST /content/publishand/content/rollbackon QA srv.hugo.qa.tomlβ sibling Hugo config for QA. Strips Joule FAB, rating, completion buttons, progress UI whensite.Params.qa = true.- QA bootstrap runbook β docs/developers/operations/qa-channel-bootstrap.md.
Rebuild workflow & admin writes β
rebuild-content.ymlmode auto-infer βgh workflow run rebuild-content.yml -f slug=Xauto-infersmode=slug-targetedwheninputs.modeis defaultfullAND a slug input is set. Don't pass-f mode=slug-targeted. Onlyworkflow_dispatchauto-infers;repository_dispatch(admin auto-trigger) usessrv/lib/_classify-rebuild-mode.js. Wall-clock:catalog-only~5min,slug-targeted~2min,full~10min. Runbook: rebuild-content-workflow.md.GITHUB_DISPATCH_TOKENenv var β Read bysrv/lib/rebuild-trigger.js; admin saves debounce-dispatchrebuild-content.ymlafter 60s. Sourced fromDISPATCH_TOKENGitHub Actions secret (notGITHUB_DISPATCH_TOKENβ GH reservesGITHUB_prefix). All four mtaext placeholders resolve at deploy time viaenvsubstwritingdeploy/<env>.resolved.mtaext. Rotation: github-dispatch-pat-rotation.md.- Alert saves do NOT trigger rebuilds β Alerts are runtime-served via
/api/alerts*. Rebuild classifier returnsmode: 'none'forAlerts(_classify-rebuild-mode.js). Cache-bust on save is the only freshness mechanism; up-to-60s delay expected.
Content model quirks β
- Tag labels are DB-driven; slugs are the join key β Frontmatter carries raw slugs (
software-product>sap-s-4hana). At Hugo build,fetch-tutorials.tsfetches slugβlabel map from/build/tag-labels, emitsdisplayTags(label) +displayTagSlugs(slug) into frontmatter +_nav.json. Navigator filter equality, license detection, topic categorization usedisplayTagSlugs; rendering usesdisplayTags. Labels admin-edited at/admin-ui/#tags-display. Missing slug falls back to lossyhumanizeTag(). Seed from legacy AEM Solr:npm run seed-tag-labels. - Categories taxonomy is fixed in v1 β 8 categories seeded via
db/data/com.sap.developers.ims-Categories.csvwith stable UUIDs. Admins editlabel/sortOrder/seedDescriptionbut cannot add/remove. - Categories reclassify is destructive β Admin
classifyCategoriesand per-OP "Classify this item" DELETE-then-INSERT junction rows. Manual category edits survive only until next reclassify run. - Tutorial slugs are lowercase canonical β Hugo emits lowercase URLs; read path 301-redirects mixed-case (see
srv/lib/content-store.js:694). Write path lowercases viatutorialsTableInfohelper. Source markdown filenames may ship with capitals; never compare slugs to publish payload without.toLowerCase(). Mismatches manifest as "0 steps" on group SSR. Repair: scripts/repair-mixed-case-tutorial-duplicates.cjs.
AI features β
- AI code-check (issue #171, behind
ChatSettings.codeCheckEnabled) β Author opt-in via[CODECHECK_N]blocks in rules.vr; trimmed spec ships in Hugo frontmatter, full spec inCodeCheckSpecs. Inline UI hits/api/codecheck(XSUAA, 30/hr/user, 5/5min/step); alsocheckCodeJoule chat tool. Persistence:CodeCheckSubmissions. Spec: 2026-06-02-ai-code-check-spike-design.md. - AI-authored quizzes (issue #208, always-on as of #312) β Author opt-in via
[AUTOAUTHOR_*]inrules.vr. Post-parse expansion inscripts/fetch-tutorials.ts. Per-tutorial content-hash cache at.tutorial-cache/<slug>.ai-quiz-cache.json. Hard cap default 200 LLM calls/build (AI_AUTHOR_BUILD_CAP). Bulk-seed:npm run seed-ai-quizzes. Model switch does NOT auto-invalidate cache β delete cache file manually. Kill-switch: setAI_AUTHOR_AICORE_SERVICE_KEYempty. Eval:scripts/evaluate-ai-quizzes.ts+scripts/aggregate-ai-quiz-eval.ts. ChatSettings.ragEnabledβ Feature flag for thegetRelevantStepstool. When toggling on first time, click "Seed Embeddings Now" in Joule Chat Settings tile. Reconciliation cron at minute 17 catches drift.HYBRID_AI_TESTS=trueto opt into category-classifier hybrid test β Default hybrid runs are $0/run. This env var enablestest/hybrid/categories-classifier.test.js(one classify call per mission fixture).AICORE_EXPLAINER_GENERATOR_DISABLEDenv var β Kill-switch for homepage explainer AI generation (#759). Set'true'β all threeAdminService.generate*Explainersactions return HTTP 503. Hand-authored content survives.
Observability & load β
- Feature Flag Viewer (
/admin-ui/#featureFlags) β read-only tile listing every runtime feature flag's live resolved state (effective value, winning layer, raw db/env/default). Source of truth:srv/lib/feature-flags/registry.js; a drift test (test/unit/feature-flags-registry.test.js) fails the build when a new*_ENABLED/*_WEIGHTenv var or settings boolean is added unregistered. Known gap: the drift regex missesprocess.env[var]bracket-notation reads. - Observability metrics module (
srv/lib/metrics.js, #805) β In-memory counters/gauges/reservoirs drained every 5min bysrv/jobs/metrics-rollup-job.jsintoMetricSnapshots. Env flags:METRICS_ENABLED(defaulttrue; kill-switch),METRICS_DB_WRAP(defaultfalse; installs passivecds.db.run/cds.db.txwrapper). Rollup does NOT usejob-lock; retention (30d/90d) does. Live snapshot:/admin-ui/#metrics,GET /admin/getMetricsSnapshot(),GET /admin/metrics/live. See observability.md. - Load tests (
test/load/) are k6, not Vitest, and do NOT run on PRs β Five scenarios drive deployed DEV. CI runs weekly (Mon 03:00 UTC) + manual. Never on push/PR (DEV quota isn't free). Thresholds intest/load/config.js; never hardcode ms in scenarios. Aborts if/content/hashesshows publish in flight. Runbook: load-testing.md.
Runtime env vars β
CONTENT_API_KEYenv var β Required forPOST /content/publishandPOST /content/rollback. Set in CI secrets and locally. Without it, publish returns 401.SUBMISSION_SALT_SECRETenv var β Required bysrv/lib/feedback-salt.jsfor hashing submitter IPs onPOST /feedback/submit. Express bridge returns 503 if missing.
Data privacy β
@cap-js/data-privacydeferred, annotations shipped (#960) β Plugin install rolled back at 0.6.2 due to twocds build --productioncrashes. Annotation cleanups landed anyway. When retrying plugin adoption: verifycds build --productionsucceeds against schema FIRST; pick up Tasks 7/8/9 blueprints; do NOT re-annotate BranchDecisions asDataSubjectDetails. Spec: 2026-07-04-960-data-privacy-plugin-design.md.
Migration β
- Change tracking suppression for REST migrators β
x-migration-mode: trueheader sent bymigrate-reference-data.jsandmigrate-user-progress.js. HANA-to-HANA path (migrate-from-hana.js) still fires DB-level changelog triggers β see migration-from-ims.md for mitigations.
Personalization β
- Personalization endpoint MUST set
X-Personalization: 1andCache-Control: private, no-storeβ the approuter is documented to never cache this header combination. Dropping either header silently allows a shared cache to serve one user's personalized payload to another user or to anonymous visitors. The smoke test (test/smoke/homepage-personalized.test.js) asserts both headers on every deployed environment. - Client-side ETag round-trip lives in
sessionStorage['sap-devs-homepage-personalized']β clearing sessionStorage forces the coordinator to fetch fresh (noIf-None-Matchheader, 200 response). The session key issap-devs-homepage-personalized; the bypass flag issap-devs-homepage-default. Both are sessionStorage (not localStorage), so they clear on tab close.
Cron jobs β
Reshuffle-video-rotation cron is TRUNCATE + INSERT β must run inside a single transaction.
srv/jobs/reshuffle-video-rotation.jsusescds.txto wrapDELETE FROM HomepageVideoRotation+ bulk INSERT. If a future refactor splits these into two top-leveldb.run(...)calls, a mid-cycle failure will empty the sidecar and visitors will see anchor-only tiles until the next successful cron pass β silently. #1031.kg-community-labelsjob skips stable clusters β nightly LLM spend is near-zero after first backlog (issue #1126).srv/jobs/kg-community-label-job.jsruns at 04:12 UTC (after Louvain at 03:57). It upsertsKgCommunityLabelrows keyed oncommunityFingerprint; if a community'smemberSlugsHash(SHA-256 of sorted member slugs) matches the stored value, the row is skipped without an LLM call. First-run ramps the full backlog over several nights becausecommunityLabelLlmBudgetPerDay(default 50) caps daily spend. The budget counter resets daily:communityLabelLlmCallsToday/communityLabelLlmCallsCountedOnonChatSettings. If the job runs but Louvain has not yet populatedKgCommunity,summariesis empty β no LLM calls β no error. Tool is gated bycommunityPeersEnabledonChatSettings(defaultfalse), enabled viaPATCH /admin/ChatSettings(<ID>)(Admin-gated; the/admin-ui/#jouleJoule Settings page edits the same singleton but does not yet list this flag). No env var reads it.
Devtoberfest β
- Devtoberfest banner is admin-uploadable β per-
DevtoberfestConfigDevtoberfestBannercomposition (wide WebP BLOB,uploadBanner/clearBanneractions), served anonymously atGET /api/devtoberfest/bannerfor the active row; the Vue island renders it as the hero with the CTA overlaid lower-right, falling back to the CSS gradient header when unset. Full deploy required (schema + admin bundle + approuter). Spec:docs/superpowers/specs/2026-07-29-devtoberfest-banner-upload-design.md.
Tutorial Navigator β
- Navigator "Featured" rail is curated via
/admin-ui/#/operationsβ Featured Tasks β draft CRUD (pick items by title viaFeaturedTaskCandidatesvalue-help, unique per item, order defaults to next integer); SSR frombrowse.json'sfeatured[]array (mission-curated or first-6-missions fallback when empty); live-rehydrated fromGET /build/featured(ETag/304, 60s server cache, mixed tutorial/mission/group types); cache busts automatically onFeaturedTaskssave/delete viaresetFeaturedCache().
Top Gotchas β full detail (relocated from CLAUDE.md) β
These paragraphs used to live inline in CLAUDE.md's "Top Gotchas" section. They were moved here verbatim to keep CLAUDE.md lean; the headline + load-bearing one-liner + a link back to this section remain in CLAUDE.md.
Build artifacts & lifecycle hooks β
ignore-scripts=truesilencespostbuild:appsβ build artifacts wired into it are NOT produced by localnpm run build:allβ the global npmrcignore-scripts=truemeans npm lifecycle hooks never fire. Thepostbuild:appshook is where the #1604 island-fingerprint step (build:island-manifest, which writeshugo/data/island_manifest.json) and 8 static guards live. During a localbuild:all, none of them run. Symptom class: fresh JS/CSS compiles (Vite emitsnavigator-<hash>.js) but is never referenced βhugo/layouts/partials/island-src.htmlfalls back to the unhashed/js/<name>.js, Hugo bakes the stale path, and the approuter ships old bundles sitting next to the new ones. Merged fixes look "not deployed" even though the deploy succeeded. CI dodges this becausedeploy.yml/unit-tests.ymlrunnpm run postbuild:appsas an explicit step (see deploy.yml:217-223 comment). Fix (2026-08-10):build:allnow callsnpm run build:island-manifestexplicitly (not via the hook), andscripts/deploy-mta.cjsStep 2.5 fails the deploy ifhugo/public/index.htmlbakes only unhashed island paths while a Vite manifest exists. Rule: any build artifact (not just a guard) needed for a correct ship must be an explicit step inbuild:all, never left to apost*/pre*lifecycle hook.build:page-fallbackis an explicitbuild:allstep (NOT a lifecycle hook) βscripts/build-page-fallback.cjscopies in-scope page snapshots fromhugo/publicintosrv/page-fallback/<key>.<ext>afterbuild:hugoruns. Becauseignore-scripts=truesilences allpre*/post*hooks, it is wired as an explicitnpm run build:page-fallbackin thebuild:allchain, positioned right afterbuild:hugo. If you add a new in-scope page toIN_SCOPE_PAGESinsrv/lib/page-key-map.js, the snapshot is picked up automatically on the next full build. Snapshots are gitignored (srv/page-fallback/*except.gitkeep); the directory is committed empty and populated at build time.
Completions rollup (issue #1934) β
- Group/Mission completions are rollup-derived β the CAP rewrite never carried over the legacy IMS TUTORIALβGROUPβMISSION rollup, so GROUP/MISSION
TaskRecordsstopped being created at the 2026-08-10 cutover.srv/lib/completion-rollup.jsrecomputes parent group(s)/mission(s) after any TUTORIAL/PUZZLE/CHECKPOINT/PETOBERFEST completion β called from_updateTutorialProgress,resetTutorialProgress, the CHECKPOINT edge ofcreateTaskRecord(developer-service),puzzle-service, andpetoberfest-upload. Slot model: alt-groups (#172) collapse to one slot where any branch satisfies; a nested GROUP slot needs all its tutorials. Records key on(user_ID, taskLegacyId=<Groups|Missions>.legacyId, taskType)and are upserted (SELECT-then-UPDATE-or-INSERT) withstampSubmissionIdso they carry the NGDS dedup key. NGDS auto-send fires on the β COMPLETED edge (GROUP/MISSION are the only NGDS-eligible rollup types). The orchestrator never throws into the completion tx. Backfill (post-cutover only):scripts/backfill-group-mission-completions.mjs(bulk,--dry-run/--since/--user, no NGDS send) thenscripts/backfill-ngds-send.mjs(rate-limited, resumable viaImsConfig 'ngds.backfill.cursor', honors env=prod + kill-switch + epoch + canonical-sapId; receiver dedups onsubmissionIdCompleted). Pre-cutover completions are intentionally NOT re-minted (legacy IMS credited them; the NGDS epoch guard suppresses them).completion-rollup.jsis NOT acontent-store.jsdependency β nosrv-qacpentry needed.
Content model β mutable ContentCurrent (Option B, #2017 / Workstream D) β
- Content served from mutable
ContentCurrent, not the oldContentFilessnapshot-per-version β three env flags gate the migration, all default OFF, flip in order:CONTENT_DELTA_WRITE_ENABLED(publish dual-writes changed slugs βContentCurrent+ append-onlyContentHistory, fail-safe) β seed via a full force rebuild (-f mode=full -f force-publish=truedual-writes all slugs; no separate migration) βCONTENT_DELTA_READ_ENABLED(serve/readers hitContentCurrent, per-slug fallback toContentFiles) βCONTENT_DELTA_SKIP_CARRYFORWARD(publish skipscarryForwardUnchangedβ O(changed) publish; rollback then replaysContentHistoryintoContentCurrent, not clear+fallback). Measured DEV: publish commit ~62sβ973ms (PROD carry-forward was ~95s @ 11k files). Serve source headerX-Content-Source: db-current(ContentCurrent) vsdb(legacy). Revert = flipSKIP_CARRYFORWARDoff +cf restart. Flags insrv/lib/feature-flags/registry.js; readers viaresolveContentBlob(srv/lib/content-store.js). LOB reads stay rawdb.run(). Cutover cleanup (deletecarryForwardUnchanged+ContentFiles) deferred to a release after PROD soak.
NGDS auto-send β
- NGDS auto-send is PROD-only + DB-gated (double gate) β automatic push of task completions to NGDS (badging/gamification) fires from
srv/lib/ngds-autosend.js#maybeAutoSendCompletion, called at the two completion transition points insrv/developer-service.js(_updateTutorialProgress+createTaskRecord). Sends ONLY when BOTH gates pass: (1) CFspace_name==='prod'(resolveDeployEnvironment, not spoofable headers) AND (2)ImsConfigkeyngds.autosend.enabled==='true'(admin kill-switch viaAdminService.toggleNgdsAutoSend; 60s flag cache, busted on toggle). Edge-only (fires on the transition β COMPLETED, never on repeat saves) and task-type-allowlisted to TUTORIAL/GROUP/MISSION (legacy parity β PUZZLE/PETOBERFEST/STEP never sent). Fails CLOSED (config read error β disabled) and never throws into the completion tx (a send fault queues inNGDSFailedMessagesfor the 2h retry job). Bulk recompute (raw HANA MERGE) + migration (raw SQL) bypass the service layer, so they cannot flood NGDS. Payload shape itself is the legacyMessageModel(#1473). Default OFF in every env; enable in PROD via the admin toggle. Status:AdminService.getNgdsAutoSendConfig()returns{enabled, environment, effective}.
Knowledge graph feature flags β
All default OFF and DEV-only unless noted. Toggles fail-open on every fault path.
KG_PAGERANK_ENABLED(issue #916) β when'true',rankNeighborhoodinsrv/knowledge-graph-service.jsmultiplicatively blends per-tutorial PageRank (weight *= 1 + Ξ± Γ normPR) into all three tutorial-targeted arms (prerequisitesOf,sharedConcepts,whatToLearnNext) and sortsteachesby concept-side PageRank. Scores recomputed nightly at 03:53 UTC bysrv/jobs/kg-pagerank-job.jsβ PageRank runs in Node.js (not HANA GraphScript β that engine ships no PageRank primitive) overKG_PG_VERTICES_V+KG_PG_EDGES_V, materialized intoConceptRank/TutorialRanksidecars. Fail-opens on every fault path (missing sidecars, HANA hiccup, empty maps β multiplier collapses to 1.0). Toggle:cf set-env tutorials-srv KG_PAGERANK_ENABLED true && cf restart tutorials-srv. Blend strength viaKG_PAGERANK_ALPHA(default1.0β weights grow at most 2Γ).KG_WCC_ISOLATION_THRESHOLD(issue #918) β nightlysrv/jobs/kg-wcc-job.jsruns at 04:07 UTC and materializes rows intoKgIsolationfor concept + tutorial vertices whose weakly-connected-component size β€ threshold. Default1;0empties the table on the next run (effectively disables the "Isolated" red-badge column on the admin Concepts + Tutorials LRs). Compute is Node.js union-find overKG_PG_VERTICES_V+KG_PG_EDGES_Vβ same reason as #916 that HANA GraphScript ships no WCC primitive (SCC yes, WCC no). Fail-quiet at read time: theafter('READ')decorators onKnowledgeGraphService.ConceptsandAdminService.Tutorialscatch any SELECT throw and leaveisolatedunset β Fiori rendersnullboolean as no badge. Toggle:cf set-env tutorials-srv KG_WCC_ISOLATION_THRESHOLD 2 && cf restart tutorials-srv(or0to disable).KG_ONDEMAND_ENABLED/KnowledgeGraphSettings.onDemandExtractionEnabled(issue #948) β whentrue,expandSearchConceptsfire-and-forgets an enqueue on zero-seed queries; a new 2-minute cronkg-ondemand-draincosine-ranks the corpus and extracts concepts from top-K tutorials viaextractConceptsFromTutorial. Coalesces near-duplicate queries; per-user (default 3/hr) and global (default 20/hr) rate-limit caps. Env knobs:KG_ONDEMAND_USER_MAX_PER_HOUR,KG_ONDEMAND_GLOBAL_MAX_PER_HOUR,KG_ONDEMAND_DRAIN_BATCH(default 3),KG_ONDEMAND_TUTORIALS_PER_REQ(default 5),KG_ONDEMAND_MAX_ATTEMPTS(default 3). Admin surface:/admin-ui/#kgOnDemand. Drain uses try/finally to recover stuck RUNNING rows on UPDATE failure. On-demand extraction is now link-only (#1115) β it attaches existing concepts (0.7 floor) but never mints. Toggle: fliponDemandExtractionEnabled=trueat/admin-ui/#kg-settings(orcf set-env tutorials-srv KG_ONDEMAND_ENABLED true && cf restart tutorials-srv).- KG community detection (issue #917) β Louvain community detection over
KG_PG_WORKSPACEruns nightly at 03:57 UTC (srv/jobs/kg-communities-job.js) via HANA GraphScriptCommunities_Louvainindb/src/procedures/KG_LOUVAIN_GRAPH.hdbprocedure. Memberships materialize into theKgCommunitysidecar (db/knowledge-graph-communities.cds). Admin surface:/admin-ui/#kgCommunitiesrenders a FE List Report (aggregated summary) + Object Page overAdminService.KgCommunitiesandAdminService.KgCommunityMembers.promoteCommunityToMission(communityId, missionSlug, title)action (SuperAdmin-gated) drafts aMissionsrow +CompletionPaths+CompletionPathItemssortedTutorials.title ASC, withMissions.sourceKgCommunityIdset so already-promoted communities can be filtered out. Nightly job fail-opens; empty sidecar renders as FE "No data", never a 500. No env flag β tile is always visible to XSUAATutorial.Authorscope. DEV-only in v1; PROD rollout deferred. Metrics:kg_communities_{duration_ms,count,max_size,failures}. KG_RETIRE_ORPHANS_ENABLED/KG_RETIRE_ORPHANS_AGE_DAYS(issue #1115) β nightlysrv/jobs/kg-retire-orphans-job.jsat 04:37 UTC flipsConcepts.statusACTIVEβRETIRED for concepts with zero links across all 10 link tables andfirstSeenAtolder thanKG_RETIRE_ORPHANS_AGE_DAYS(default 14). RETIRED falls out of every read path (all filterstatus='ACTIVE'positively). First-run retirement ramps rather than purging instantly. Reversible:cf set-env tutorials-srv KG_RETIRE_ORPHANS_ENABLED false(off) or bulkUPDATE Concepts SET status='ACTIVE' WHERE status='RETIRED'(data revert). A re-proposed retired slug is reactivated in-tx bykg-merge-on-write.js(retiredBySlug+action:'reactivated').- KG community peers + community labeling (issue #1126) β
communityPeersEnabledonChatSettings(defaultfalse) gates thefindCommunityPeersJoule tool (srv/lib/kg/joule-tool-community-peers.js). When enabled, the tool accepts atutorial_slug, looks up the anchor'scommunityFingerprintinKgCommunity, and returns sibling tutorials from the same Louvain cluster plus the LLM-generated cluster label fromKgCommunityLabel. Nightlykg-community-labelsjob (srv/jobs/kg-community-label-job.js) runs at 04:12 UTC (after Louvain at 03:57) and LLM-names each community with β₯ 2 tutorials. Identity key iscommunityFingerprint(String(64)); skip-key ismemberSlugsHash(SHA-256 of sorted slugs) β stable member sets incur zero LLM calls. Daily budget iscommunityLabelLlmBudgetPerDayonChatSettings(default 50). Fail-open per community. Toggle:communityPeersEnabledis aChatSettingscolumn (NOT an env var), enabled by an Admin viaPATCH /admin/ChatSettings(<ID>)on the AdminService singleton (the/admin-ui/#jouleJoule Settings page edits the same entity but does not yet surface this flag). DEV-only until PROD Louvain data verifies. Metrics:kg_community_label_{duration_ms,labeled,skipped,failures}. KG_COMMUNITY_WEIGHT(issue #1171) β when> 0,SearchService.before('READ')appends a SECOND additive rank term+ KG_COMMUNITY_WEIGHT * (case slug when '<peer>' then 1.0 else 0 end)alongside the existing concept-overlapKG_WEIGHT(#945). Peers are tutorials sharing a LouvaincommunityFingerprint(#917/#1126) with the top-COMMUNITY_TOP_K(5) concept-overlap hits. Default0(OFF) βbuildCommunityRankFragmentinsrv/lib/search-kg-signal.jsshort-circuits before any DB fetch and the rank SQL is byte-identical to the #945 formula. Only fires whenChatSettings.searchKgRerankEnabled=true. Fail-open (any DB throw β term collapses to''). Membership fetched packet-safe (β€5 fingerprints.in(), members capped 200, filtered in Node). Regression harness + churn report attest/harness/community-rank-churn*; do NOT enable in any env before the ON-vs-OFF churn is hand-reviewed. Toggle:cf set-env tutorials-srv KG_COMMUNITY_WEIGHT 1.5 && cf restart tutorials-srv(withsearchKgRerankEnabled=true).KG_COMMUNITY_COVERAGE_NUDGE_THRESHOLD(issue #1172) β theafter('READ','KgCommunities')decorator insrv/admin-service.jscomputes, per community at read time, mission-coverage % + dominant published mission + orphan-tutorial count (helper:srv/lib/kg-community-coverage.js) and populates virtual fields onAdminService.KgCommunities. Coverage is published-missions-only and the % denominator is tutorial members only (concept/tag-only communities render N/A, not 0%).coverageHigh(>= threshold, default 70) is the single server-computed flag driving both the LR criticality badge and the FE promote-timeMessageBox.warning("~X% already in<mission>β extend instead?"). Fail-quiet in its own try/catch: any throw β warn-log, fields unset, no badge, never a 500. No new job/table/migration β computed live. Packet-safe: the covered-slug.in()is chunked at 500. SuperAdmin gate onpromoteCommunityToMissionunchanged; the nudge is advisory. Override:cf set-env tutorials-srv KG_COMMUNITY_COVERAGE_NUDGE_THRESHOLD 80 && cf restart tutorials-srv. DEV-only until the #1126 PROD Louvain rollout lands.- Cluster-level Q&A in Joule (issue #1173) β
describeCommunityJoule tool (srv/lib/kg/joule-tool-describe-community.js) answers "what's the AI cluster?" / "everything around RAP" by resolving a free-text topic to a labeled Louvain community. LLM-side matching:communityCatalogLayerinsrv/lib/chat-context.jsinjects the labeled-cluster catalog (fromKgCommunityLabel, cached ~5min, cap 40) into the learner system prompt only whencommunityPeersEnabledis true; the model passes the chosen label asmatched_label, andmatchLabel(srv/lib/kg/community-label-match.js, pure) does case-insensitive exact match + token-overlap fallback + ambiguity detection. Reuses the existingcommunityPeersEnabledflag (NO new flag/schema), thecommunity-peers-cardsSSE frame +renderCommunityPeersCardsrender path, and the extractedresolveCommunityMembershelper (srv/lib/kg/community-members.js). Fail-open throughout (never 500). Gotcha:buildSystemPromptLinesinchat-orchestrator.jsis DEAD at runtime βbuildSystemPrompt(chat-context.js) never calls it; the live guidance ships viacommunityCatalogLayer. DEV-only until PROD Louvain data verifies.
HCQL protocol adapter (issue #995) β
- HCQL protocol adapter β CAP 10 beta feature.
@hcqlannotation on 9 read-heavy services (AdminService, AuthorService, AnalyticsService, ExportsService, ConsolidationService, KnowledgeGraphService, HomepageService, SearchService, DeveloperService) insrv/hcql-enablement.cdsexposes each service at its existing OData URL to also accept CQNSELECTbodies (HCQL and OData share URLs; dispatch is by request-body shape). Auth inherited from existing@readonly/@requires. Writes intentionally unsupported (beta not stable cross-runtime). Runtime hazard: CAP 10.0.3 exits the process on malformed CQN β do not expose to untrusted clients until CAP hardens the adapter. Kill switch: deletesrv/hcql-enablement.cds,cds build --production, redeploy. Full reference: hcql-support.md.
Freshness detector β
- Freshness detector grounding needs the corpus-embedding backfill β the
checkFreshness/freshness-scanengine cosine-searchesApiDocs/Samplesembeddings. Those columns are populated bysrv/jobs/freshness-corpus-embedding-job.js(nightly17 3+ on-demandrunJob). Until it runs in an env, grounding returns nothing and every API-obsolescence claim degrades toconfidence: Low(fail-open, by design). LLM calls use the SAP AI SDK directly (@sap-ai-sdk/orchestration, forced tool-call), NOT@cap-js/ai; unit tests injectglobalThis.__FRESHNESS_TEST_IMPL__. Bulk scan gated byFRESHNESS_SCAN_ENABLED(default OFF). Tutorial markdown is sourced fromContentFiles.sourceContentviagetTutorialSource(slug)insrv/lib/content-store.jsβ NOT fromSteps.description(Steps are never populated with step markdown; reading Steps would yield nothing). Findings carry a globalcodeBlockIndexacross the whole-tutorial markdown β per-step attribution is deferred because the persisted source is not split per step.
Signed provenance envelope (issue #2245) β
PROVENANCE_ENVELOPE_ENABLEDis a DB config flag (ImsConfig), default OFF, DEV-first β controlled viaImsConfigkeyflag.provenance.envelope(registered insrv/lib/feature-flags/registry.js). When OFF,GET /content/tutorials/:slug/provenancereturns 404 andGET /.well-known/tutorial-provenance/jwks.jsonreturns 404. No env var alternative; never store the signing key as an env var directly (use the credstore β see below). Flip via/admin-ui/#featureFlags(DEV); confirm PROD behaviour before enabling there.Two anonymous public endpoints β both are plain Express routes registered in
srv/server.js, intentionally outside any CAP service.@requires/@restrictdo not apply. Both are read-only content-distribution endpoints, safe to serve unauthenticated:GET /content/tutorials/:slug/provenanceβ returns{ jws, jwks_url }.jwsis a compact Ed25519-signed JWS (JWT serialisation viajose'sSignJWT) whose payload attests{ sub, contentHash, sourceCommit, builtAt, freshness: { confidence, runAt, openHighCount, openMediumCount } }. Returns 404 when the flag is OFF, 404 when the slug is unknown, or 503 on signing failure (fail-open: content still serves normally).GET /.well-known/tutorial-provenance/jwks.jsonβ returns the Ed25519 JWKS{ keys: [...] }for out-of-band JWS verification. Returns 404 when the flag is OFF. Key ID (kid) in the JWKS matches thekidheader in every issued JWS, enabling key rotation without re-verifying old tokens.
PROVENANCE_SIGNING_KEYcredstore secret β the Ed25519 private key (PKCS8 PEM format) is stored in the target environment's BTP Credential Store asPROVENANCE_SIGNING_KEY.srv/lib/provenance-keys.jsreads it credstore-first viaresolveSecret('PROVENANCE_SIGNING_KEY')(credstore βprocess.envfallback β null), the same seam asCONTENT_API_KEYβ a CF credstore binding does not auto-inject the value intoprocess.env, so the env fallback only fires for local/dev where the PEM is exported directly. (Before #2308 this module readprocess.envdirectly and never saw a credstore-provisioned key β empty JWKS + 503 on PROD.) Never commit a key or paste one into.mtaext, env files, or source. Rotation: generate a new key (see below), store it via/admin-ui/#secrets(the per-env credstore rotation flow), thencf restart tutorials-srvβ the newkidpropagates to the JWKS automatically on next request. Old JWS tokens signed with the retired key will fail verification once the key is removed from the JWKS; that is expected.Generating a DEV signing key β run locally and copy the PEM output, then paste it into
/admin-ui/#secretsasPROVENANCE_SIGNING_KEYon the target env. Never write the output to a file you might commit.bashnode -e "import('jose').then(async j=>{const {privateKey}=await j.generateKeyPair('EdDSA',{crv:'Ed25519',extractable:true});console.log(await j.exportPKCS8(privateKey))})"Freshness confidence values β derived by
deriveConfidenceinsrv/lib/provenance-freshness.js. Possible values:high(freshness report DONE within 30 days, no open findings),medium(DONE but 30β90 days old or has open medium-severity findings),low(DONE but > 90 days old or any open high-severity finding),unknown(no freshness report or report status not DONE). Corpus-embedding backfill must have run before any value other thanunknowncan be returned β see "Freshness detector" above.Advisory headers on the HTML serve path β when the flag is ON,
GET /content/tutorials/:slugalso setsX-Freshness-Confidence: <value>andX-Content-Provenance: <jwks_url>on the HTML response. These are advisory only; caching behaviour is unchanged. Any error deriving the advisory is swallowed silently (fail-open).sourceCommitplumbing βContentCurrent.sourceCommit(String(64)) is populated by the publish pipeline via thesource_commitfield in the publish payload;scripts/fetch-tutorials.tsthreads the HEAD commit SHA through to the publish client. On older published content the column is NULL; the provenance JWS payload will carrysourceCommit: nullin that case, which is valid.