Skip to content

Release 1.26.0 — DEV → main (PROD promotion) - #2305

Merged
jung-thomas merged 180 commits into
mainfrom
DEV
Sep 14, 2026
Merged

jung-thomas merged 180 commits into
mainfrom
DEV

Conversation

@jung-thomas

Copy link
Copy Markdown
Contributor

Promotes DEV to main for the 1.26.0 production cutover.

Includes the release-prep commit (#2304): What's New digest refreshed (+40 entries) and MTA version bumped 1.25.0 → 1.26.0, plus all other DEV changes merged since the 1.25.0 release.

Deploy from a fresh origin/main after merge (never from a feature branch).

Follow-on to #1904. A stale session no longer always surfaces as the
200-HTML XSUAA login interstitial — AppRouter/CAP can now reject the
mutating request (or the /auth/user handshake) with a genuine 401
Unauthorized. csrfFetch only treated a followed redirect or a 2xx-HTML
body as session-expiry, so the 401 was returned to the caller; the
tutorial Done button's apiPost saw res.ok === false, returned false, and
reverted with no re-login (POST /api/completeStep 401 in the console).

Treat a 401 as the stale-session tell in isSessionExpiredResponse so
csrfFetch forces the same top-level /login?returnTo navigation and the
OAuth redirect actually executes. 403 is deliberately excluded (CSRF
retry signal / Akamai edge block) and the safe-method GET path is
untouched, so read 401s (anonymous /auth/user, getProgress) still pass
through.
…ession-401

fix(csrf): re-authenticate on a real 401 in a stale session (#2239)
The /tutorials/<slug>.md endpoint served relative image paths, so an
agent/LLM consuming the .md off-domain could not resolve them.

Rewrite relative image paths to absolute raw.githubusercontent.com URLs
at serve time, using repo+branch from RepoCatalog (keyed by slug,
populated on publish, covers live tutorials). This avoids the schema
change / re-publish / backfill a publish-time fix would need, and keeps
stored sourceContent pristine. Provenance is fail-open: no RepoCatalog
row leaves image paths relative (pre-#2235 behavior).

Mirrors the render-time rule in scripts/parsers/images.ts: skips
absolute http(s) and ../ paths, strips leading ./ or /, resolves under
tutorials/<slug>/ (covers both images/ subdir and flat abap layout).

Direct raw.githubusercontent.com image src URLs are not Akamai-blocked
(only encoded URLs in a u= query are).
…md-image-urls

feat(md): absolutize image URLs in served tutorial markdown (#2235)
…#2240)

Consume the SURVEYURL column now computed in the planner's DTF_SESSION_V1
view (base URL from the new DB-backed AppConfig singleton + session code).
Add SURVEYURL to the Session facade, the schedule route SELECT whitelist,
and map it to surveyUrl in the assembled feed. Render a "Session Survey"
link in the detail panel only (not grid cards); empty when unset.

Depends on the planner producer change (developer-relations/devtoberfest-planner
PR #62) being deployed first.
feat(devtoberfest): Session Survey link in schedule detail panel (#2240)
… choice (#2244)

Slice the source markdown per step (Option B) alongside the existing HTML
slicer, and add a `format` param ('markdown' default | 'html') that selects a
single body representation to avoid doubling agent context.

- srv/lib/tutorial-markdown-steps.js (new): pure ESM per-step markdown splitter
  (v2 H3 steps, fence- and HTML-comment-aware), ports build-time parser logic;
  strips image-directive comments + [VALIDATE_n]/[DONE].
- srv/lib/tutorial-step-slicer.js: sliceStepMarkdown() — version-keyed cache
  (slice-md: key, shares slice-slug:<slug> tag so content.published invalidates
  both), single-column sourceContent BLOB read, gunzip, per-step
  absolutizeImagePaths via RepoCatalog provenance. HTML sliceStep untouched.
- srv/lib/mcp-developer-tools.js: format validated against ['markdown','html'],
  branches to sliceStep/sliceStepMarkdown; returns {slug, stepNumber, stepTitle,
  content, contentFormat, textLength, totalSteps} — collapses html->content.
- srv/{developer,search}-service-mcp.cds: format param + collapsed return shape,
  updated doc-comments (both MCP tiers).
- srv/mcp/mcp-manifest.js: get_tutorial_step description reflects format choice.
- Tests: new pure parser suite (9); slicer md-agreement + image-strip + null;
  progress/recommend/contract tools cover default+markdown+html+400.

cp-list audit: tutorial-markdown-steps.js sits downstream of tutorial-step-slicer.js
(already absent from srv-qa cp list, boot-safe) and is not reachable from
content-store.js — no mta.yaml change required.
The check-slug-lookups static guard flagged the new getRepoProvenance
.where({ slug }) lookup. The MCP handler lowercases the slug (lcSlug)
before it reaches the slicer, so annotate with slug-canonical:
caller-canonicalizes.
Re-land HCQL (issue Option 1) scoped to the 5 authenticated services,
on a @sap/cds 10.0.3 -> 10.1.0 bump with explicit per-service @protocol
path isolation (HCQL at /hcql/<svc>, OData path unchanged). Spike verified
the collision that caused the #1004 revert and the clean-landing config.
Phase A CAP ecosystem to latest-compatible-with-cds-10.1 (required),
Phase B broader tree within-major only (majors deferred). Full suite +
build gate each phase. Records maintainer decisions.
…ial URL

Agents/LLMs that send `Accept: text/markdown` to /tutorials/<slug> now
get the normalized Markdown source (via the existing markdownServeHandler)
instead of HTML, without rewriting the URL to the .md variant.

- New pure helper prefersMarkdown() honours q-values; browsers (no
  text/markdown in Accept) are unaffected.
- Vary: Accept set on BOTH the HTML and markdown branches so the edge
  cache keys on the header and cannot poison one representation with the
  other.
- Non-tutorial slugs (concept-/topic-/puzzle-/channel-/group-/mission-/__)
  never negotiate, matching the existing Link-alternate guard.
…2246)

Expose an anonymous HTTP function + MCP tool on SearchService that embeds
caller TEXT server-side and returns scored content references only — never
raw vectors, never accepts a caller-supplied vector.

- semanticSearchEnabled ChatSettings flag (default OFF; 503 when off),
  registered in feature-flags/registry.js
- corpora: tutorials | concepts | external | all (cosine over stored
  Vector(1536); HANA COSINE_SIMILARITY, SQLite JS-cosine)
- topK clamped [1,50]; minScore default from ChatSettings; query-embedding
  cached by hash(query+model)
- per-IP rate limiter (429) reusing DB-backed SearchSettings budget
- MCP manifest tool entry + @requires:'any' CDS function
- unit + handler tests (SQLite); caches on globalThis to survive Windows
  Vitest module duplication across served vs imported instances
…d-step

feat(mcp): expose per-step markdown via get_tutorial_step with a format choice (#2244)
…negotiation

feat(content): Accept: text/markdown negotiation on primary tutorial URL
…ch-2246

# Conflicts:
#	test/unit/mcp-contract.test.js
…2246

feat(search): public anonymous semantic_search over content corpus (#2246)
Mirror CAP/Capire's sitemap.md with a human- and AI-navigable map:
verb-lane navigation plus every mission expanded to its ordered
tutorials. Distinct from sitemap.xml (flat crawler XML) and
llms-full.txt (flat metadata dump) — this is a navigable hierarchy.

- scripts/fetch-sitemap-catalog.ts: build-time GET /build/catalog fetch
  into hugo/data/sitemap_catalog.json; fail-open (empty -> /missions/
  index fallback) when CAP_BASE_URL absent, like llms.txt.
- hugo.toml: sitemapmd output format on home; dedicated
  text/x-web-markdown media type so the file is written as .md.
- sitemapmd.md template: Navigation / Missions / Topics / Reference.
- Cross-links from llms.txt, public AGENTS.md, ai-consumption.md (#16).
- Smoke test for /sitemap.md (sparse-catalog tolerant).
feat(ai): add navigable /sitemap.md site map
…2249)

@cap-js/ord 1.9.1 dropped protocols missing from its CAP→ORD protocol
map, so the 5 MCP-enabled services never appeared in the generated ORD
document. 1.9.2 added `mcp` to the map; 1.9.3 is the current patch.

Verified via `cds compile srv --to ord`: apiResources now include 5 mcp
entries (AdminService, DeveloperService, HomepageService,
KnowledgeGraphService, SearchService) alongside odata-v4/graphql/rest,
with no "Unknown protocol 'mcp'" warnings. Remaining websocket skips
(DisplayService, EventStreamService) are expected — not an ORD protocol.
Adds sourceCommit : String(64) to ContentFilesAspect, ContentCurrentAspect,
and ContentHistoryAspect in db/_content-shape.cds. Threads the per-slug
sourceCommits map from appendHandler in content-store.js through
appendToSession in content-publish-session.js, stamping it on the
ContentFiles entry and carrying it forward to ContentCurrent/ContentHistory
in dualWriteCurrentAndHistory (both HANA SQL and SQLite CQL paths).
Generates migration=3 for ContentFiles (ALTER TABLE ADD NVARCHAR(64)).
Bug A (dark mode): SkillTreeMap nodes, Lesson choice cards, Results and
landing/coming-soon text hardcoded light-mode backgrounds with no explicit
color, so under the site's `html.dark` they inherited white text on a white
card (light-on-light, unreadable). Add scoped `html.dark` overrides across
SkillTreeMap.vue, Lesson.vue, Results.vue and App.vue. Verified WCAG contrast
on the live DEV page: unlocked 4.95, done 4.69, locked 4.18 (dimmed/disabled).

Bug B (completion/navigation): the active Lesson screen had no way back to the
map, and a missed drill showed "Try Again" even though the engine re-queues
the beat to the end and advance() always moves forward — so it read as
"starting over on the same lesson". Add a "Back to map" control to Lesson.vue
(new `back` emit wired to App's existing handleBack) and relabel the
wrong-answer button to "Got it - continue" to match the actual behaviour.
Engine semantics unchanged.
fix(ktt): dark-mode contrast + lesson back button and honest labels
Bug C: a lesson was marked "mastered" whenever the beat queue emptied,
regardless of score — so answering every drill wrong still showed the
success banner and banked XP, then silently returned to the map ("started
over"). Mastery is now score-gated client-side: correct / distinct-drills
must be >= 0.7 (PASS_RATIO). Below threshold shows a dedicated retry screen
and banks nothing (no XP, no streak, no mastered).

XP was awarded (+10) but never surfaced. New StatsBar on the map shows the
XP total, a cat-themed level tier (Kitten -> TLA Sage), and the streak, so
XP is both meaningful and visible.

- engine.ts: PASS_RATIO, totalDrills, scoreRatio, passed
- progress.ts: levelForXp + KttLevel tiers
- Lesson.vue: complete emits { lessonId, xp, passed, correct, total }
- Results.vue: dual pass/fail template; fail shows score + Try Again
- StatsBar.vue (new): XP / level / streak
- App.vue: fail path banks nothing + retry remount via :key
- tests: 42 passing across engine/progress/Lesson/App
fix(ktt): gate mastery on 70% pass + surface XP levels/streak
- ABAP (Advanced Business Application Programming) → Unit 2, lesson 2 (next to RAP)
- BDC (SAP Business Data Cloud) → Unit 3, lesson 2 (next to DWC)
- New Unit 5 'SAP & AI' (🤖, order 5):
  - Lesson 5.1 AI Foundations: AI, ML, LLM, RAG
  - Lesson 5.2 SAP's AI Stack: BAIP, NLP, ISLM, MCP
- New 'ai' category; regenerated ktt_lessons.json + KttLessons seed CSV
- Tests: derive lesson count from CSV; allow up to 5 units (was hardcoded 12/4)
feat(ktt): add ABAP, BDC + new Unit 5 'SAP & AI' lessons
beginSession was the one publish HTTP call in the happy path with neither
withRetry nor a try/catch, so a transient HANA pool-acquire 500 ('Pool
resource could not be acquired within 1s') bubbled to the top-level Fatal
catch and aborted the whole prod rebuild. Wrap begin (and the delta-detection
/content/hashes fetch) in withRetry with jittered backoff, mirroring the
existing commit posture; 500/502/503/504 are already classified transient.

Also add cds.requires.db.pool config (acquireTimeoutMillis 5000, max 100) for
hana [hybrid]/[production] so brief pool contention waits instead of erroring
at the 1s default.

Tests: beginSession under withRetry recovers from a transient pool-timeout
500 and does not retry a permanent 409.
fix(publish-content): retry transient HANA pool-timeout 500s (#2286)
…2287)

normalizeListContinuationFences only matched fences at exactly 4 spaces.
A partial list-marker outdent can leave the continuation fence at 5-7
spaces (the #2287 repro was 6), which Goldmark still treats as a
doc-level indented code block -> literal backticks + a never-closed
block that swallows all following steps.

Generalize the opening-fence match to 4-7 spaces and strip (indent - 3)
leading spaces from the fence and its content/close lines, bringing the
delimiter to 3 while preserving relative indentation. 8+ spaces are left
alone (genuine deeper nesting). Existing safety guards unchanged:
matching close required, idempotent, blank-line preserving.

Adds regression fixtures at 5/6/7-space indents plus 8-space no-op,
relative-indent preservation, idempotency, and unterminated cases.
…nce-4-7

fix(list-fence): repair list-continuation fences indented 4–7 spaces (#2287)
…eenshot edits publish (#2288)

Same-filename image byte changes are invisible to the content publish
delta detector: publish-content.ts hashes rendered HTML + source markdown,
never the image bytes. Images are referenced by an external
raw.githubusercontent.com URL via /img-cdn?u=<encoded>, so editing a
screenshot in place (unchanged filename → unchanged URL → unchanged HTML
hash) never triggers a republish — the stale image is served indefinitely.
Text-only and text+image edits work because the HTML hash moves; renaming
the image works because the URL (and thus the HTML) changes. Image-only,
same-filename edits are the blind spot the issue reports.

Fix: on a slug-targeted rebuild, re-warm just the changed tutorial's images
through the already content-aware ingest endpoint (POST /content/image
dedups on image bytes). Changed bytes are re-stored; unchanged ones report
'unchanged'. Scoped to slug-targeted (not full) rebuilds — full rebuilds
re-warm the whole catalog already. Best-effort (continue-on-error) so a
warm failure never fails the content rebuild. Covers both prod/dev/qa
(rebuild-content.yml) and the QA author-preview channel
(rebuild-content-qa.yml).

backfill-images.ts gains --slug (scope to one tutorial's built HTML) and
--channel qa (reads CAP_QA_BASE_URL/CONTENT_API_KEY_QA + hugo/public-qa,
mirroring publish-content.ts), plus a main-module guard so the pure
enumeration helpers are unit-testable.
…changes

fix(images): republish images on slug rebuilds when edited in place (#2288)
Replace invalid tutorials-admin@example.sap with thomas.jung@sap.com and
add Developer Tutorial Authors MS Teams channel link.
The participate QR fell back to a bare external
https://developers.sap.com/app-space.html when no participateUrl
param was set, dropping the event ID. AppSpace.vue reads eventId
from the URL query, so scanners landed in the default app space
instead of the event's instance.

Build the fallback as ${origin}/app-space?eventId=<id> so the
barcode carries the event through. Display label updated to match.

Fixes #2295
…mpletions (#2294)

getEventBuckets returns { eventName, eventType, hasLogo, buckets:[...] }
since #2133, but event-stream.ts still treated the response as an array
and called .map on the object -> 'K.map is not a function'.

Read json.buckets (with legacy array fallbacks). Also drop the
empty-buckets early-return: an event with zero completions is not an
error (404 events are already rejected server-side), so proceed to the
Socket.IO connection and let late completions render live.
…-buckets

fix(display-app): getEventBuckets object shape + zero-completions (#2294)
…s-email

fix(admin): correct NoAccess contact info (#2293)
…tid-qr

fix(display-app): QR carries eventId to /app-space (#2295)
…pace (#2296)

Event descriptions are admin-authored (Events.description, @UI.MultiLineText)
and shown on the App Space hero, but were rendered escaped as plain text, so
Markdown syntax appeared literally.

- Extract the existing markdown-it config (html:false, breaks, linkify) from
  catalog-renderer.js into a shared srv/lib/markdown.js so the safety policy
  lives in one place; catalog-renderer now imports it (no behaviour change).
- developer-service.js getAppSpaceProgress returns eventDescription as rendered
  safe HTML (raw HTML in the source is escaped by html:false — no injection).
- AppSpace.vue renders it via v-html in a <div> (markdown emits block <p>;
  <p>-in-<p> is invalid), with :deep() child-margin resets for scoped styles.
- Admin label hint: "Description (Markdown)" + QuickInfo tooltip.

Tests: srv renderMarkdown unit (format + HTML-escape + coercion); AppSpace
v-html render test (guards regression back to escaped {{ }}).
buildConceptsPayload issued its ~11 independent link-table reads (teaches,
edges, journeys, blogs, missions, videos, apiDocs, samples, helpDocs, events)
strictly sequentially. On HANA that is ~11 serial round-trips over multi-
thousand-row tables per cold call, pushing /build/concepts to ~50-60s and past
the 30s smoke-gate timeout (4 smoke tests failed as pure timeouts on DEV
1.25.0; endpoint returned correct 200s, just slowly).

The reads are all keyed off idsSet (computed once in step 1) with no inter-
query dependencies, so wrap them in a single Promise.all wave. Steps 3+4 stay
grouped (they jointly build requires/relatedTo/requiredBy) and now run their
two edge reads in parallel too. Stitch (step 6) and groupBy unchanged; same
columns, filters, sorts, and per-concept caps.

No user impact was in play - /concepts/* pages are served from HANA BLOBs
(fast); this endpoint feeds only the Hugo concept-render build pipeline and
the smoke gate. Not a 1.25.0 regression (query helper last changed 2026-08-09);
the slowness is structural and pre-existing.

All 744 concepts-related unit tests pass; payload shape unchanged.
perf(concepts): parallelize /build/concepts link-table reads
New catalog-renderer import is a content-store transitive dep; srv-qa would
boot then crash on first render without it.
…own-2296

feat(events): Event description Markdown → safe HTML on App Space (#2296)
Add AdminService.copyMission(ID) action that deep-copies a Mission into a
new unpublished Mission: the mission row, every CompletionPath and its
CompletionPathItems, and the MissionTags/MissionCategories link rows.
Referenced Tutorials/Groups/Tags/Prizes are linked (via *_ID), never
duplicated. Slug becomes <slug>-copy (disambiguated on collision against
the @assert.unique.slug index). published/legacyId/community identity are
reset. All writes run in the request tx.

Wire a 'Copy Mission' List Report action (requiresSelection, single) in
app/admin/missions that POSTs the action with CSRF and deep-links the
admin-shell into the new mission's Object Page.

Tests: 2 new unit tests (deep-copy graph + slug disambiguation), 30 green.
check-slug-lookups flagged the uniqueSlug SELECT: the CompletionPaths
path probed by raw p.slug. Lowercase the base once in the helper (slugs
are lowercase-canonical repo-wide) so both the uniqueness probe and the
inserted slug are canonical; mark the probe pre-canonicalized.
feat(admin): Copy Mission — deep-copy action + LR button (#2300)
chore(release): 1.26.0 — What's New digest + version bump
@jung-thomas
jung-thomas merged commit 90f0ebe into main Sep 14, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant