Skip to content

docs(CLAUDE): Option B content-model gotcha + rollout order - #2033

Merged
jung-thomas merged 1 commit into
DEVfrom
docs/option-b-gotcha
Aug 25, 2026
Merged

docs(CLAUDE): Option B content-model gotcha + rollout order#2033
jung-thomas merged 1 commit into
DEVfrom
docs/option-b-gotcha

Conversation

@jung-thomas

Copy link
Copy Markdown
Contributor

Adds a Top-Gotchas entry documenting the Option B content model (mutable ContentCurrent + ContentHistory), the 3 env flags + flip order (write → seed → read → skip-carryforward), the seed-via-force-rebuild trick, the measured ~62s→973ms publish win, rollback replay, and revert. Docs only. Ref: Workstream D (#2021/#2028/#2032), design #2017.

@jung-thomas
jung-thomas merged commit c4b94d5 into DEV Aug 25, 2026
2 of 3 checks passed
@jung-thomas
jung-thomas deleted the docs/option-b-gotcha branch August 25, 2026 18:25
jung-thomas added a commit that referenced this pull request Aug 25, 2026
…#2038)

* freshness: give the LLM tutorial context + SAP-aware rules

The freshness scan judged code blocks in isolation and applied generic
best-practice dogma, producing false positives (intentional errors,
demo base64 credentials, dev-container setup, npm version-pinning advice
that contradicts CAP guidance).

- extractCodeBlocks now captures contextBefore/contextAfter (the prose
  paragraph adjacent to each fence).
- extractTutorialContext pulls frontmatter + Prerequisites, fed once at
  the top of the prompt so the model knows the reader's environment.
- SYSTEM_PROMPT now instructs the model to judge blocks in context,
  respect intentional teaching artifacts (deliberate errors, illustrative
  demo credentials), and follow SAP/CAP conventions (never suggest pinning
  @sap/* package versions).
- Export SYSTEM_PROMPT/buildUserMessage; add extractor + prompt tests.

* freshness: tighten system prompt for author noise + calibration

- PRECISION: prefer no finding over a speculative one; report each issue once.
- OUTPUT vs CODE: never flag staleness/secrets inside illustrative output blocks.
- SEVERITY defined by reader impact (fails today / deprecated path / cosmetic).
- SCOPE: skip prose, screenshots, product-name currency, deliberate simplifications.
- GROUNDING: quote the offending token; flag training-data inferences as Low.

Reframes the reviewer as helping the tutorial AUTHOR. Adds prompt-guard tests.

* perf(rebuild): cache node_modules to skip ~48s npm ci on unchanged lockfiles

setup-node's 'cache: npm' already warms ~/.npm, but 'npm ci' still spends
~48s extracting/linking 1680 packages + native builds into node_modules on
every rebuild run (verified in run 32783495862: cache restored, yet 'added
1680 packages in 48s'). Cache the materialized node_modules (root +
hugo-apps) on an exact lockfile key and skip the install on hit.

Exact-key only (no restore-keys): skipping 'npm ci' means a mismatched tree
would go unreconciled, so any lockfile change must miss and reinstall clean.
Safe because neither package.json has install/prepare/postinstall lifecycle
scripts -- nothing outside node_modules is produced at install time.

* docs(openspec): propose slug-targeted delta rebuild (O(changed) end-to-end)

Design + specs + tasks for making a single-tutorial rebuild scale with
changed slugs instead of the full corpus. Four independently-shippable
workstreams, grounded in code research (file:line):

- content-delta-publish: mutable ContentCurrent + append-only ContentHistory
  replaces full-snapshot-per-version + carryForwardUnchanged (~95s server cost)
- generated-content-cache: cache hugo/content/tutorials (~25MB) with a
  correctness-first key (hashes /build feeds + parser source + per-slug source)
- single-slug-render: scoped Hugo render (R2 verified coherent)
- tutorial-discovery: unmask GraphQL errors + fix App-token org-node auth +
  loud REST fallback

Companion CI win (node_modules cache) shipped separately in #2016.

* fix(fetch): unmask GraphQL discovery errors + loud REST fallback

graphqlRequest warn-logged a GraphQL errors/null-data response and returned
the null data, so callers dereferenced data.organization/data.repository and
threw an opaque TypeError — which the outer catch mistook for an outage and
SILENTLY degraded discovery to the slow per-slug REST path (~1400 extra calls,
secondary-rate-limit risk) every rebuild.

- graphqlRequest now throws a typed GraphqlError carrying the GraphQL error
  type+message; throws on errors[] or null data.
- GraphqlError.isAuthError classifies FORBIDDEN/INSUFFICIENT_SCOPES/permission
  failures (the App-installation-token org-node gap).
- discovery + metadata-batch fallbacks log at ERROR level (once) when the cause
  is an auth/permission error, so a token-scope regression is visible instead
  of hidden as latency.

Workstream A of the slug-targeted-delta-rebuild design (#2017). The auth fix
itself (repo-oriented discovery vs PAT vs App Org:Read) is Workstream B and
gated on the real error this now surfaces on DEV.

Test: test/unit/github-graphql-error-unmask.test.ts (6 cases).

* feat(rebuild): generated-content cache helpers (Workstream C foundation)

Pure, tested helpers for the slug-targeted fast path: feed fingerprint
(catalog/co-completions/tag-labels), sidecar read/write, fast-path
eligibility decision, and navEntries-by-slug reconstruction. Fail-open
throughout. Nothing calls these yet — the fetch-tutorials.ts reuse wiring
(flag-gated, requires a byte-identical DEV diff-verify) + the actions/cache
workflow step land next.

Test: test/unit/content-cache.test.ts (15 cases).

* feat(rebuild): wire generated-content fast path (Workstream C, flag-gated off)

On a slug-targeted run with CONTENT_CACHE_FAST_PATH=true, fetch-tutorials
reuses the previously-generated .md + sidecar nav/author rows for non-target
slugs, skipping composeTutorial/fetchRulesVr/AI-quiz/writeHugoPage — the bulk
of Phase 3's ~56s. Two gates, both required, else full regen:
  - actions/cache KEY = parser-source hash (a parser change → cache miss → the
    per-slug existsSync guard falls through to recompose)
  - runtime feed fingerprint over CAP catalog + tag-labels (nav/mission/tags);
    co-completions excluded (empty on warm-cache runs, recommendations are
    client-hydrated)
Sidecar (navEntries + authorRows + fingerprint) written to .content-cache/,
cached by the same parser-hashed key as hugo/content/tutorials +
image_dimensions.json. Fail-open throughout; flag defaults OFF so default
behavior is byte-identical to today.

Enabling on DEV (input content-cache=true) + a byte-identical diff-verify
(task 3.6) is the follow-up before flipping the default.

Helper unchanged behavior; content-cache tests green (15).

* feat(db): add ContentCurrent + ContentHistory aspects/entities (Option B schema, Workstream D)

Mutable current-content table (one row per slug, no version column) + append-only
per-(version,slug) history carrying the BLOB for self-contained rollback replay
(design.md D1/D2). Mirrored into the QA namespace. Verified: full model compiles +
deploys to sqlite::memory:.

NEXT (gated, not in this commit): .hdbmigrationtable staging for the two new HANA
tables (db/src/ + db/last-dev/ regen via the project's cds-build-staging procedure)
+ hybrid deploy verify — the version-counter-hazard step. Then write path (dual-write,
flag), readers, rollback.

* feat(publish): Option B dual-write to ContentCurrent + ContentHistory (Workstream D, flag-gated)

commitSession now mirrors freshly-published slugs into the mutable ContentCurrent
(UPSERT, one row per slug) + append-only ContentHistory (WRITTEN rows) when
CONTENT_DELTA_WRITE_ENABLED=true, alongside the legacy ContentFiles write.
Chunked BLOB read mirrors carryForwardUnchanged (raw db.run on HANA for the
LOB-locator path, CQL on SQLite). Fail-SAFE: never throws into the commit tx —
legacy ContentFiles stays the source of truth until the reader cutover.

Tests (SQLite): test/unit/content-delta-dualwrite.test.js (4) — one-row-per-slug,
UPSERT on republish, history append per version, flag-off writes neither.
Existing commit-path tests still green (carryforward-memory + guard, 12).
HANA LOB path is covered by the hybrid publish→rollback test (task 7.4).

* fix(#2023): stop Hugo YAML 1.1 coercing yes/no quiz options to booleans

rules.vr answer options like [ ] no / [x] yes rendered as false/true (and
a yes/no correctAnswer flipped to a boolean) because the frontmatter
serializer emits bare yes/no/on/off tokens under YAML 1.2, which Hugo's
YAML 1.1 parser then reads as booleans. jsonify ships those booleans into
<script id="tutorial-data"> and the quiz island renders "false"/"true".

Add scripts/lib/hugo-yaml.ts (hugoFrontmatterStringify, version: '1.1') so
the writer quotes every token a 1.1 reader would misparse, and route all
Hugo-frontmatter serializers through it (render-frontmatter, fetch-tutorials,
fetch-advocates, author-pages-writer). Regression test round-trips through a
YAML 1.1 read.

* fix(notify): trigger source-repo dispatch on both master and main

The canonical notify-workflow template (docs/authors/tutorial-repo-dispatch.yml)
triggered only on `branches: [main]`. Source repos are a MIXTURE of default
branches — most default to `main`, but some (e.g. sap-tutorials/Tutorials)
default to `master`. On a master-default repo a main-only trigger never fires,
so edits never dispatch `tutorial-updated` and PROD silently goes stale
(root cause of the cp-* content-drift incidents; Tutorials was hand-patched by
sap-tutorials/Tutorials#24184 but the template itself was never updated — so a
future `install-notify-workflows` run would REVERT that fix).

- Template now triggers on `[master, main]` (GitHub ignores a listed branch
  that doesn't exist, so it's safe on every repo regardless of default).
- Add a regression guard in the installer unit test asserting the template
  fires on both branches and never ships a main-only trigger.

Propagate to source repos with `npm run install-notify-workflows -- --execute`.

* fix(feature-flags): register CONTENT_DELTA_WRITE_ENABLED env flag

Pre-existing DEV breakage inherited by this PR's CI merge ref: the
feature-flag registry drift guard (test/unit/feature-flags-registry.test.js)
fails because srv/lib/content-publish-session.js reads
process.env.CONTENT_DELTA_WRITE_ENABLED (Workstream D Option-B dual-write)
without a matching FEATURE_FLAGS entry. Register it (env, true-enables,
default OFF, dev-only) so the guard passes.

* feat(serve): Option B read cutover + rollback replay (Workstream D, flag-gated, draft) (#2028)

* feat(serve): Option B read cutover for serveStoredSlug (Workstream D, flag-gated)

serveStoredSlug serves from the mutable ContentCurrent (WHERE slug=?, no
version join) when CONTENT_DELTA_READ_ENABLED=true AND the slug is present,
else falls back to the legacy version-pinned ContentFiles snapshot. Per-slug
fallback keeps a partially-populated ContentCurrent (pre-full-seed) from
404-ing slugs still in ContentFiles. LOB BLOB read stays on raw db.run for
HANA (new hanaCurrentTableName helper). X-Content-Source: db-current | db.

Covers the hot path + pages/author/advocate (all route through serveStoredSlug).
Special-slug/catalog/embedding readers still use ContentFiles (follow-up 6.2-6.5)
— consistent because ContentFiles stays fully populated + authoritative.

Tests: test/unit/content-delta-read.test.js (3); content-store legacy suite green (48).

* feat(rollback): Option B ContentCurrent clear on rollback (Workstream D, flag-gated)

During the dual-write migration window, rollbackHandler now clears ContentCurrent
after the manifest flip (when CONTENT_DELTA_WRITE_ENABLED=true) so every read
falls back to the just-restored authoritative ContentFiles(target.version) —
correct-by-fallback, no per-slug BLOB replay needed. ContentHistory is retained
(append-only) for the post-ContentFiles-retirement full history-replay (task 8.4).
Fail-safe: never breaks the legacy rollback.

Test: content-delta-read.test.js rollback case (publish v1+v2 → rollback v1 →
ContentCurrent cleared → serve falls back to ContentFiles D1). 4 tests total green.

* feat(devtoberfest): deep-link URLs for sessions grid filters/search (#2030) (#2031)

Add query-string state sync to the Devtoberfest sessions grid so search,
week/track filters, edition, and an open session panel are all shareable
and restorable from the URL:

  /devtoberfest/sessions/?q=<text>&week=<w>&track=<name>&edition=<id>&session=<id>

- New pure url-state.ts (parse/serialize + validation), mirroring the
  calendar's #2006 url-state.ts and the concepts-filter precedent;
  fail-open (empty/unknown params -> null, never throws). Unit-tested
  (round-trip, empty/whitespace rejection, unknown-param drop).
- App.vue: parse on mount (q/week/track applied synchronously; session
  applied once the feed loads), history.replaceState on state change,
  popstate re-sync. App-level integration tests cover restore-from-URL,
  session-panel open, write-back, and unknown-session fail-open.

Closes #2030

* feat: Option B reader sweep + carry-forward removal + rollback replay (Workstream D 6.2-6.5, 8.4) (#2032)

* feat(serve): migrate __404__ + __nav__ single-slug readers to ContentCurrent (Workstream D 6.2)

Adds shared resolveContentBlob helper (ContentCurrent-first when read flag on +
present, else legacy ContentFiles active snapshot; raw db.run LOB on HANA).
serveNotFound (__404__) + navHandler (__nav__) now route through it — same
per-slug fallback as serveStoredSlug, safe on a partially-seeded ContentCurrent.

Enumeration readers (nav-fallback/hashes/source-hashes/embeddings) deliberately
NOT migrated yet — they enumerate ALL slugs and must wait for the full
ContentCurrent seed (task 4.3), else they'd return only the dual-written subset.

Tests: content-store (48) + content-delta-read (4) green.

* feat(serve): migrate hashesHandler + navHandlerFallback to ContentCurrent (Workstream D 6.3)

Both enumerate the active slug set (metadata-only, no BLOB) — switch to
ContentCurrent (no version) when CONTENT_DELTA_READ_ENABLED, else legacy
ContentFiles active snapshot. Correct once ContentCurrent is fully seeded
(task 4.3, running now via a full force rebuild). Tests: 52 green.

* feat: complete Option B reader sweep — all readers off ContentFiles-version (Workstream D 6.2-6.5)

Migrates the remaining readers to ContentCurrent (read-flag gated, per-slug or
enumeration fallback to legacy ContentFiles):
- chrome-shell __shell__ (ContentCurrent-first + version fallback; version cache
  key kept — self-invalidates as manifest version still bumps per publish)
- sourceHashesHandler, getTutorialSource (source-column reads)
- embedding readContentBuffer + active-slug-set reads (embedding-stats,
  embedding-reconciliation, cleanup.pruneOrphanEmbeddings, admin seedEmbeddings)

No cache re-key needed (6.5): version-keyed caches invalidate on each publish's
manifest-version bump regardless of read source. With ContentCurrent seeded
(2362 slugs), every reader now serves from it under the read flag. This is the
prerequisite for removing carryForwardUnchanged (8.4).

Tests: content-store + delta-read + dualwrite + carryforward (58) green.

* feat(publish): Option B carry-forward removal + rollback replay (Workstream D 8.4 — the payoff)

CONTENT_DELTA_SKIP_CARRYFORWARD (default off) makes publish O(changed slugs):
commitSession skips carryForwardUnchanged, so ContentFiles(newVersion) holds
only the freshly-published slugs. ContentCurrent (mutable, dual-written) stays
complete and serves everything; the ~95s PROD carry-forward disappears.

Coupled rollback rework: with carry-forward off, ContentFiles(V) is no longer a
complete snapshot, so rollbackHandler now REPLAYS ContentHistory into
ContentCurrent (per-slug latest version <= V, chunked LOB read) instead of
clear+fallback. detectReverts/history untouched.

Adds invalidateContentCache() export (prod busts via cache-generation token on
publish; ops/tests that drive publish directly use this).

Tests: content-delta-carryforward-skip.test.js (2) — O(changed) publish
(ContentFiles holds only changed slugs) + byte-correct multi-version rollback
replay. Full content suite 70 green.

Enable order (per env): read cutover deployed + ContentCurrent seeded, THEN
flip CONTENT_DELTA_SKIP_CARRYFORWARD.

* chore(feature-flags): register CONTENT_DELTA_READ_ENABLED + SKIP_CARRYFORWARD env flags

Adds the two remaining Option B env flags to the feature-flag registry
(CONTENT_DELTA_WRITE_ENABLED was already registered). Satisfies the
feature-flags-registry drift guard. dev-only status, default OFF.

* fix(deploy): build + copy display-app into approuter static (#2035)

The canonical .deploy/mta.yaml approuter builder copied admin-ui,
analytics-ui, explore-ui, scanner-ui and data-inspector-ui but never
built or copied app/display-app, so static/display-app/ never shipped
and the XSUAA-protected /display-app/ route 404'd in every env.

Root mta.yaml had the step but that file is not what mbt build uses.

Add npm install + vite build + copy of dist/ into static/display-app/,
mirroring the analytics-ui pattern (app has base '/display-app/').

Note: the per-app deploy guards (Step 3.5 admin bundle, 3.6 QA
navigator, explore-ui check) are hand-curated allowlists and none
covers display-app/scanner-ui — an omitted route slips through.

* docs(CLAUDE): add Option B content-model gotcha (ContentCurrent + 3 flags + rollout order) (#2033)

* fix(guard): mark Option B ContentCurrent slug lookups as caller-canonicalized (#2036)

The Option B reader sweep (#2032) added new ContentCurrent read paths in
resolveContentBlob/serveStoredSlug (content-store.js) and readContentBuffer
(embedding-pipeline.js) mirroring existing ContentFiles reads, but only the
pre-existing lines carried the // slug-canonical: marker. The 8 new sibling
lookups use the identical caller-canonicalized slug (documented contracts),
so mark them consistently rather than adding redundant .toLowerCase().

Fixes check-slug-lookups build guard failure in CI.

* chore(release): bump MTA version to 1.19.0 for PROD release (#2037)
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