diff --git a/docs/authors/writing-tutorials.md b/docs/authors/writing-tutorials.md index be60eba2f..076ee306d 100644 --- a/docs/authors/writing-tutorials.md +++ b/docs/authors/writing-tutorials.md @@ -300,9 +300,10 @@ The reader sees only the variant matching their OS. Their choice persists across the browser; BAS detected when the tutorial is opened from inside Business Application Studio). After they pick an OS, that choice is remembered. -**Missing variants.** When a step doesn't cover the reader's chosen OS, the platform shows the -closest match and a small banner: "No Linux instructions for this step — showing macOS." Use -this when one OS path is genuinely identical to another. +**Missing variants.** When a step doesn't cover the reader's chosen OS, the platform shows a +placeholder message — "No `` instructions for this step." — instead of substituting another +OS's content. No automatic closest-match substitution is made. To avoid gaps, include a panel for +every OS you expect readers to use, or omit the group entirely if the step is OS-agnostic. **Existing OS-tabbed tutorials get the picker for free.** No author migration required — the heuristic detects OS-flavored OPTION blocks automatically. diff --git a/packages/mcp/tutorial-step-slicer.js b/packages/mcp/tutorial-step-slicer.js index 44223119c..706ade7a4 100644 --- a/packages/mcp/tutorial-step-slicer.js +++ b/packages/mcp/tutorial-step-slicer.js @@ -17,10 +17,22 @@ import * as metrics from '@tutorials/core/metrics.js'; import { isFlagEnabled } from '@tutorials/core/feature-flags/db-flags.js'; import { parseMarkdownSteps } from '@tutorials/core/tutorial-markdown-steps.js'; import { absolutizeImagePaths } from '@tutorials/core/tutorial-markdown.js'; -import { renderDirectivesAsText } from '../../scripts/parsers/directives.js'; import { filterVariants } from './option-filter.js'; import { collectTroubleshootEntries } from '../../scripts/parsers/blockquote-troubleshoot-collect.js'; +// Lazy import: scripts/parsers/directives.js is a TypeScript source file that is NOT +// bundled into gen/srv at cds build time. At CF runtime the workspace path is absent; +// the pre-built _shared/parsers.bundle.mjs (created by scripts/bundle-shared.cjs, same +// pattern as freshness-detector.js after #3029) is used instead. +// Memoised so the async resolution only happens once per process lifetime. +let _directivesMod; +async function getDirectivesMod() { + if (_directivesMod) return _directivesMod; + try { _directivesMod = await import('../../scripts/parsers/directives.js'); } + catch { _directivesMod = await import('./_shared/parsers.bundle.mjs'); } + return _directivesMod; +} + const NS = 'com.sap.developers.ims'; const LOG = cds.log('mcp-slicer'); @@ -256,6 +268,7 @@ async function loadAndParseMarkdown(slug) { } const { repo, branch } = await getRepoProvenance(slug); + const { renderDirectivesAsText } = await getDirectivesMod(); const steps = new Map(); for (const s of parsedSteps) { const md = renderDirectivesAsText(absolutizeImagePaths(s.markdown, { slug, repo, branch })); diff --git a/scripts/lint-rules/__tests__/authoring-aids.test.ts b/scripts/lint-rules/__tests__/authoring-aids.test.ts index 8a6011dba..a0854717f 100644 --- a/scripts/lint-rules/__tests__/authoring-aids.test.ts +++ b/scripts/lint-rules/__tests__/authoring-aids.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { authorNotesInProdRule, qaOnlyInProdRule } from '../authoring-aids.js'; +import { authorNotesInProdRule, qaOnlyInProdRule, promotionWarningRule } from '../authoring-aids.js'; describe('authorNotesInProdRule', () => { it('finds an inline author note', () => { @@ -35,3 +35,24 @@ describe('qaOnlyInProdRule', () => { expect(qaOnlyInProdRule.scan('test', ['Hello.'], ['Hello.'])).toHaveLength(0); }); }); + +describe('promotionWarningRule', () => { + it('finds a promotion-warning directive in PROD source', () => { + const lines = [':::promotion-warning[resolve before PROD]', 'This step needs a screenshot refresh.', ':::']; + const findings = promotionWarningRule.scan('test', lines, lines); + expect(findings).toHaveLength(1); + expect(findings[0].severity).toBe('notice'); + expect(findings[0].rule).toBe('promotion-warning-in-prod-source'); + expect(findings[0].line).toBe(1); + expect(findings[0].message).toContain('promotion-warning'); + }); + + it('does NOT flag a promotion-warning pattern mid-line (must be fence-opener)', () => { + const lines = ['Some text :::promotion-warning inline']; + expect(promotionWarningRule.scan('test', lines, lines)).toHaveLength(0); + }); + + it('returns empty for clean source', () => { + expect(promotionWarningRule.scan('test', ['Hello.'], ['Hello.'])).toHaveLength(0); + }); +}); diff --git a/scripts/lint-rules/authoring-aids.ts b/scripts/lint-rules/authoring-aids.ts index f4d5570b7..8eb691d56 100644 --- a/scripts/lint-rules/authoring-aids.ts +++ b/scripts/lint-rules/authoring-aids.ts @@ -81,3 +81,41 @@ export const qaOnlyInProdRule: Rule = { return findings }, } + +/** + * promotionWarningRule — notices when PROD source contains `:::promotion-warning` blocks. + * + * A `:::promotion-warning` block is an author-placed marker indicating content that should + * be reviewed or resolved before the tutorial is promoted to PROD. It is analogous to + * `:::qa-only` but is intended to flag specific concerns rather than hide content. + * + * Assumption: the `:::promotion-warning` directive has not yet been deployed to the + * platform as of this rule's addition; this rule pre-declares the lint gate for when + * authors begin using the pattern. Owners should confirm the exact directive syntax + * once it ships and adjust the regex if needed. + * + * Severity: 'notice' (advisory — never blocks a build even with --strict). + */ +export const promotionWarningRule: Rule = { + id: 'promotion-warning-in-prod-source', + describe: 'PROD source contains `:::promotion-warning` blocks. These are author-placed "do not promote" markers that should be resolved before PROD publication.', + + scan(slug, _lines, rawLines): LintFinding[] { + const findings: LintFinding[] = [] + + for (let i = 0; i < rawLines.length; i++) { + if (/^:::promotion-warning/.test(rawLines[i])) { + findings.push({ + rule: 'promotion-warning-in-prod-source', + slug, + file: slug, + line: i + 1, + message: '`:::promotion-warning` block found in PROD source. Resolve the flagged concern and remove the block before promoting to PROD.', + excerpt: rawLines[i].trim(), + severity: 'notice', + }) + } + } + return findings + }, +} diff --git a/scripts/lint-rules/data/cli-deprecations.json b/scripts/lint-rules/data/cli-deprecations.json index 794581172..eca89afbd 100644 --- a/scripts/lint-rules/data/cli-deprecations.json +++ b/scripts/lint-rules/data/cli-deprecations.json @@ -17,6 +17,36 @@ "replacement": "Use `no-route: true` in manifest.yml", "docs": "https://docs.cloudfoundry.org/devguide/deploy-apps/manifest-attributes.html#no-route" }, + { + "tool": "cf", + "match": "cf\\s+map-route\\s+.*--hostname", + "severity": "warn", + "since": "cf-cli v8", + "message": "The `--hostname` flag was removed from `cf map-route` in cf CLI v8. Pass the full route as a positional argument instead.", + "replacement": "cf map-route --hostname → cf map-route .", + "docs": "https://docs.cloudfoundry.org/devguide/deploy-apps/routes-domains.html", + "needsOwnerReview": true + }, + { + "tool": "cf", + "match": "cf\\s+create-route\\s+.*--hostname", + "severity": "warn", + "since": "cf-cli v8", + "message": "The `--hostname` flag was removed from `cf create-route` in cf CLI v8. Specify the full hostname directly.", + "replacement": "cf create-route --hostname → cf create-route .", + "docs": "https://docs.cloudfoundry.org/devguide/deploy-apps/routes-domains.html", + "needsOwnerReview": true + }, + { + "tool": "cf", + "match": "cf\\s+push\\s+.*--no-route\\b", + "severity": "warn", + "since": "cf-cli v8", + "message": "The `--no-route` flag was removed from `cf push` in cf CLI v8. Set `no-route: true` in manifest.yml instead.", + "replacement": "Add `no-route: true` to manifest.yml", + "docs": "https://docs.cloudfoundry.org/devguide/deploy-apps/manifest-attributes.html#no-route", + "needsOwnerReview": true + }, { "tool": "cds", "match": "cds\\s+serve\\s+--in-memory", @@ -26,6 +56,16 @@ "replacement": "cds watch", "docs": "https://cap.cloud.sap/docs/tools/" }, + { + "tool": "cds", + "match": "cds\\s+run\\b", + "severity": "warn", + "since": "@sap/cds 6", + "message": "`cds run` was deprecated in favour of `cds watch` for local development. `cds watch` also monitors for file changes and restarts automatically.", + "replacement": "cds watch", + "docs": "https://cap.cloud.sap/docs/tools/", + "needsOwnerReview": true + }, { "tool": "npm", "match": "npm\\s+install\\s+--save\\b", @@ -34,5 +74,15 @@ "message": "`npm install --save` is the default since npm 5 and the flag is unnecessary.", "replacement": "npm install ", "docs": "https://docs.npmjs.com/cli/v10/commands/npm-install" + }, + { + "tool": "npm", + "match": "npm\\s+install\\s+-g\\s+@sap/cds\\b", + "severity": "warn", + "since": "@sap/cds-dk 7", + "message": "Install the CAP CLI via `@sap/cds-dk`, not `@sap/cds`. `@sap/cds` is the runtime library; `@sap/cds-dk` is the development kit that provides the `cds` command.", + "replacement": "npm install -g @sap/cds-dk", + "docs": "https://cap.cloud.sap/docs/get-started/", + "needsOwnerReview": true } ] diff --git a/scripts/lint-rules/deprecated-cli.ts b/scripts/lint-rules/deprecated-cli.ts index ddb53b3e1..c3b3e1568 100644 --- a/scripts/lint-rules/deprecated-cli.ts +++ b/scripts/lint-rules/deprecated-cli.ts @@ -3,6 +3,15 @@ * * Scans bash/sh/shell/console/terminal/powershell/cmd fences for commands * matching known deprecated patterns from cli-deprecations.json. + * + * QUARTERLY REVIEW REMINDER (spec task 4.4): + * Each quarter, review cli-deprecations.json for: + * - New CF CLI v8+ removals: https://docs.cloudfoundry.org/cf-cli/cf-help.html + * - New CDS/CAP CLI changes: https://cap.cloud.sap/docs/releases/ + * - New BTP CLI changes: https://help.sap.com/docs/btp/sap-business-technology-platform/btp-cli + * - New MBT CLI changes: https://sap.github.io/cloud-mta-build-tool/ + * Rules marked `needsOwnerReview: true` should be verified and the flag removed + * once an owner confirms the deprecation against the official changelog. */ import { createRequire } from 'node:module' import type { LintFinding } from '../lint-tutorial-markdown.js' @@ -17,6 +26,10 @@ interface CliDeprecation { message: string replacement: string docs: string + /** When true, the rule was added based on best-effort research and needs + * an owner to verify against the official changelog before treating as + * authoritative. The rule still fires — it's advisory at 'warning' level. */ + needsOwnerReview?: boolean } const DEPRECATIONS: CliDeprecation[] = require('./data/cli-deprecations.json') diff --git a/scripts/lint-tutorial-markdown.ts b/scripts/lint-tutorial-markdown.ts index 2d6d685e0..bb11b69ed 100644 --- a/scripts/lint-tutorial-markdown.ts +++ b/scripts/lint-tutorial-markdown.ts @@ -32,7 +32,7 @@ import { iframeNonAllowlistedHostRule } from './lint-rules/iframe-non-allowliste import { authorNameWithoutProfileRule } from './lint-rules/author-name-without-profile' import { directiveUnknownRule } from './lint-rules/directive-unknown' import { deprecatedCliRule } from './lint-rules/deprecated-cli.js' -import { authorNotesInProdRule, qaOnlyInProdRule } from './lint-rules/authoring-aids.js' +import { authorNotesInProdRule, qaOnlyInProdRule, promotionWarningRule } from './lint-rules/authoring-aids.js' import { directiveSuggestUiRule } from './lint-rules/directive-suggest-ui' import { directiveSuggestKbdRule } from './lint-rules/directive-suggest-kbd' @@ -209,7 +209,7 @@ const fenceIndentDiscipline: Rule = { }, } -const RULES: Rule[] = [indentedNumberedListItem, fenceIndentDiscipline, iframeNonAllowlistedHostRule, authorNameWithoutProfileRule, directiveUnknownRule, deprecatedCliRule, authorNotesInProdRule, qaOnlyInProdRule, directiveSuggestUiRule, directiveSuggestKbdRule] +const RULES: Rule[] = [indentedNumberedListItem, fenceIndentDiscipline, iframeNonAllowlistedHostRule, authorNameWithoutProfileRule, directiveUnknownRule, deprecatedCliRule, authorNotesInProdRule, qaOnlyInProdRule, promotionWarningRule, directiveSuggestUiRule, directiveSuggestKbdRule] /** * Rule: branch-syntax (issue #172 PR 3).