@shoalrun/cli 0.1.0-alpha.13 → 0.1.0-alpha.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/DEPLOYMENT-PROFILES.md +287 -0
  3. package/README.md +5 -2
  4. package/agent-docs/.well-known/shoalrun/navigation/project-updates-50f26fe22670.txt +5 -1
  5. package/agent-docs/SNAPSHOT.json +5 -5
  6. package/agent-docs/changelog/2026-10-30.md +43 -0
  7. package/agent-docs/changelog/2026-11-03.md +56 -0
  8. package/agent-docs/changelog.md +17 -1
  9. package/agent-docs/documents.json +419 -61
  10. package/agent-docs/index.md +73 -103
  11. package/agent-docs/provenance.json +4 -4
  12. package/agent-docs/publish/content-export.md +1 -1
  13. package/agent-docs/publish/content-export.sections/prerequisites.md +11 -0
  14. package/agent-docs/publish/deployment-profiles.md +273 -0
  15. package/agent-docs/publish/deployment-profiles.sections/before-you-begin.md +44 -0
  16. package/agent-docs/publish/deployment-profiles.sections/build-and-inspect-the-edition.md +69 -0
  17. package/agent-docs/publish/deployment-profiles.sections/check-the-result.md +46 -0
  18. package/agent-docs/publish/deployment-profiles.sections/choose-navigation-labels.md +53 -0
  19. package/agent-docs/publish/deployment-profiles.sections/current-limits.md +46 -0
  20. package/agent-docs/publish/deployment-profiles.sections/define-the-shared-sku-manifest.md +70 -0
  21. package/agent-docs/publish/deployment-profiles.sections/html-markdown-and-agents.md +44 -0
  22. package/agent-docs/publish/deployment-profiles.sections/mark-pages-and-shared-sections.md +135 -0
  23. package/agent-docs/publish/deployment-profiles.sections/related-reading.md +42 -0
  24. package/agent-docs/publish/deployment-profiles.sections/supply-the-profile-from-the-deployment.md +68 -0
  25. package/agent-docs/search-index.json +1 -1
  26. package/agent-docs/start.md +2 -2
  27. package/agent-docs/start.sections/add-the-page-to-navigation.md +1 -1
  28. package/agent-docs/start.sections/build-and-check-both-outputs.md +1 -1
  29. package/agent-docs/start.sections/build-this-site.md +1 -1
  30. package/agent-docs/start.sections/check-writing-style.md +1 -1
  31. package/agent-docs/start.sections/create-a-page.md +1 -1
  32. package/agent-docs/start.sections/make-it-yours.md +1 -1
  33. package/agent-docs/start.sections/migrating-from-github-packages.md +1 -1
  34. package/agent-docs/start.sections/prerequisites.md +1 -1
  35. package/agent-docs/start.sections/related-reading.md +1 -1
  36. package/agent-docs/start.sections/start-your-own-project.md +2 -2
  37. package/dist/api-publication-plan.d.ts +3 -0
  38. package/dist/api-publication-plan.js +30 -3
  39. package/dist/artifact-bundle.d.ts +1 -0
  40. package/dist/artifact-bundle.js +1 -1
  41. package/dist/authoring-api.d.ts +1 -1
  42. package/dist/authoring-api.js +1 -1
  43. package/dist/authoring-project-command.js +4 -2
  44. package/dist/authoring-project.d.ts +2 -0
  45. package/dist/authoring-project.js +4 -4
  46. package/dist/authoring-snapshot.d.ts +23 -4
  47. package/dist/authoring-snapshot.js +84 -12
  48. package/dist/authoring-validation-command.js +5 -3
  49. package/dist/authoring-validation.d.ts +2 -0
  50. package/dist/authoring-validation.js +38 -11
  51. package/dist/cli-discovery.d.ts +2 -2
  52. package/dist/cli-discovery.js +9 -9
  53. package/dist/cli.js +8 -6
  54. package/dist/compiler.d.ts +13 -0
  55. package/dist/compiler.js +25 -74
  56. package/dist/component-names.js +1 -1
  57. package/dist/component-transform.js +7 -1
  58. package/dist/config.d.ts +4 -1
  59. package/dist/config.js +19 -5
  60. package/dist/content-export-command.d.ts +13 -1
  61. package/dist/content-export-command.js +3 -2
  62. package/dist/content-export.d.ts +13 -0
  63. package/dist/content-export.js +5 -4
  64. package/dist/content-source.d.ts +1 -0
  65. package/dist/content-workspace.d.ts +13 -0
  66. package/dist/content-workspace.js +18 -1
  67. package/dist/deployment-profile.d.ts +52 -0
  68. package/dist/deployment-profile.js +277 -0
  69. package/dist/frontmatter.js +11 -4
  70. package/dist/page-materialization.js +2 -0
  71. package/dist/page-renderer.d.ts +1 -0
  72. package/dist/page-renderer.js +3 -1
  73. package/dist/palettes.d.ts +1 -1
  74. package/dist/project-navigation.d.ts +5 -0
  75. package/dist/project-navigation.js +19 -0
  76. package/dist/publication-plan.d.ts +3 -1
  77. package/dist/publication-plan.js +23 -4
  78. package/dist/publication-render.d.ts +2 -1
  79. package/dist/publication-render.js +2 -2
  80. package/dist/site-identity.d.ts +3 -0
  81. package/dist/site-identity.js +69 -0
  82. package/dist/vercel.d.ts +1 -0
  83. package/dist/vercel.js +3 -1
  84. package/package.json +2 -1
  85. package/skills/shoalrun-author/SKILL.md +5 -1
  86. package/skills/shoalrun-author/references/customer-editions.md +164 -0
  87. package/skills/shoalrun-recipes/SKILL.md +2 -0
  88. package/skills/shoalrun-setup/SKILL.md +2 -0
  89. package/styles/site.css +7 -3
  90. package/agent-docs/publish/content-export.sections/upgrade-an-existing-integration.md +0 -34
@@ -1,3 +1,4 @@
1
+ import { pruneFeatureTree, profileSelection } from './deployment-profile.js';
1
2
  import { readFile, realpath } from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
@@ -53,7 +54,7 @@ const visitTree = (node, visit) => {
53
54
  };
54
55
  /** Export selected source content; never invoke the website shell/compiler. */
55
56
  export async function exportContent(configFile, destination, options = {}) {
56
- const loaded = await loadConfig(configFile);
57
+ const loaded = await loadConfig(configFile, { profile: options.profile });
57
58
  const resolved = resolvedCompilerConfig(loaded.value);
58
59
  // Export has no masthead, so a website title is optional in a content-only project.
59
60
  if (resolved.site !== undefined && !isConfigMapping(resolved.site))
@@ -150,7 +151,7 @@ export async function exportContent(configFile, destination, options = {}) {
150
151
  throw new Error(`Unknown export snippet ${selection.snippetId}`);
151
152
  // This is an explicit isolated context. It has no containing page, operations,
152
153
  // relationships, page variables, or inherited publication/access metadata.
153
- const snippetDocument = { file: definition.file, body: '', tree: root([]), variant: publication,
154
+ const snippetDocument = { featureContext: loaded.deployment?.context, file: definition.file, body: '', tree: root([]), variant: publication,
154
155
  route: `/__content_snippets/${encodeURIComponent(selection.id)}`, markdownRoute: '',
155
156
  frontmatter: { id: selection.id, title: selection.title, summary: selection.summary, type: 'snippet', slug: '/', access: { policy: 'public' } } };
156
157
  const checkSnippet = (id, seen = new Set()) => {
@@ -160,7 +161,7 @@ export async function exportContent(configFile, destination, options = {}) {
160
161
  const source = snippets.get(id);
161
162
  if (!source)
162
163
  throw new Error(`Unknown snippet ${id}`);
163
- visitTree(source.tree, node => {
164
+ visitTree(pruneFeatureTree(source.tree, loaded.deployment?.context, source.file), node => {
164
165
  if (['RelatedReading', 'DocCard', 'DocCards', 'DocCollection', 'CookbookChooser', 'ApiExample', 'ApiSchema', 'FrontmatterReference', 'Anchor'].includes(node.name))
165
166
  throw new Error(`Snippet instance ${selection.id}: <${node.name}> needs page context`);
166
167
  if (node.name === 'Snippet') {
@@ -455,6 +456,6 @@ export async function exportContent(configFile, destination, options = {}) {
455
456
  }
456
457
  await commitArtifacts(path.resolve(destination), artifacts, undefined, 'export');
457
458
  return { status: unreviewed.length ? 'needs-review' : 'ok', output: path.resolve(destination), publication, formats: target.formats,
458
- resources: records.length, documents: records.filter(item => item.ref.kind === 'page').length, documentIds: records.flatMap(item => item.ref.kind === 'page' ? [item.ref.documentId] : []), comparison, assets: assets.records.size, digest: bundleDigest,
459
+ profileSelection: profileSelection(loaded.deployment), resources: records.length, documents: records.filter(item => item.ref.kind === 'page').length, documentIds: records.flatMap(item => item.ref.kind === 'page' ? [item.ref.documentId] : []), comparison, assets: assets.records.size, digest: bundleDigest,
459
460
  apiReview: reviews.map(row => ({ apiId: row.apiId, digest: row.digest })), diagnostics };
460
461
  }
@@ -50,6 +50,7 @@ export type Frontmatter = {
50
50
  [key: string]: unknown;
51
51
  };
52
52
  export type Document = {
53
+ featureContext?: import('./deployment-profile.js').FeatureContext;
53
54
  change?: ResolvedChange;
54
55
  subjectDeprecation?: ResolvedSubjectDeprecation;
55
56
  file: string;
@@ -33,6 +33,7 @@ export declare function prepareContentWorkspace({ base, basePath, config, loaded
33
33
  snippets: Map<string, SnippetDefinition>;
34
34
  apiPlan: {
35
35
  api: import("./api.js").ApiReader;
36
+ operationRequirements: Map<string, unknown>;
36
37
  queryRules: Map<string, import("./api.js").QuerySyntaxRule[]>;
37
38
  lifecycleMappings: Map<string, {
38
39
  from: string;
@@ -66,6 +67,8 @@ export declare function prepareContentWorkspace({ base, basePath, config, loaded
66
67
  id: string;
67
68
  reference: string;
68
69
  sourceOperationId?: string;
70
+ shared?: unknown;
71
+ requires?: unknown;
69
72
  }>;
70
73
  generateReferences?: boolean | {
71
74
  pathPrefix?: string;
@@ -107,6 +110,8 @@ export declare function prepareContentWorkspace({ base, basePath, config, loaded
107
110
  id: string;
108
111
  reference: string;
109
112
  sourceOperationId?: string;
113
+ shared?: unknown;
114
+ requires?: unknown;
110
115
  }>;
111
116
  generateReferences?: boolean | {
112
117
  pathPrefix?: string;
@@ -149,6 +154,8 @@ export declare function prepareContentWorkspace({ base, basePath, config, loaded
149
154
  id: string;
150
155
  reference: string;
151
156
  sourceOperationId?: string;
157
+ shared?: unknown;
158
+ requires?: unknown;
152
159
  }>;
153
160
  generateReferences?: boolean | {
154
161
  pathPrefix?: string;
@@ -186,6 +193,8 @@ export declare function prepareContentWorkspace({ base, basePath, config, loaded
186
193
  id: string;
187
194
  reference: string;
188
195
  sourceOperationId?: string;
196
+ shared?: unknown;
197
+ requires?: unknown;
189
198
  }>;
190
199
  generateReferences?: boolean | {
191
200
  pathPrefix?: string;
@@ -227,6 +236,8 @@ export declare function prepareContentWorkspace({ base, basePath, config, loaded
227
236
  id: string;
228
237
  reference: string;
229
238
  sourceOperationId?: string;
239
+ shared?: unknown;
240
+ requires?: unknown;
230
241
  }>;
231
242
  generateReferences?: boolean | {
232
243
  pathPrefix?: string;
@@ -279,6 +290,8 @@ export declare function prepareContentWorkspace({ base, basePath, config, loaded
279
290
  id: string;
280
291
  reference: string;
281
292
  sourceOperationId?: string;
293
+ shared?: unknown;
294
+ requires?: unknown;
282
295
  }>;
283
296
  generateReferences?: boolean | {
284
297
  pathPrefix?: string;
@@ -1,3 +1,4 @@
1
+ import { matchesFeatures } from './deployment-profile.js';
1
2
  import path from 'node:path';
2
3
  import { readFile } from 'node:fs/promises';
3
4
  import { parse } from './content.js';
@@ -9,7 +10,7 @@ const keyFor = (id, variant) => `${id}|${variant.channel}|${variant.version}|${v
9
10
  /** Shared source resolution. No website navigation, styles, hosting or provider setup. */
10
11
  export async function prepareContentWorkspace({ base, basePath, config, loaded, variants, language, readerFilters, contentFilter, metadataProvider, metadataDependencies, sourceOverrides, authoringFiles, allowRouteCollisions }) {
11
12
  const { docs, knownDocumentIds, knownDocumentTypes, authoredOperationIds, routeOwners, contentDir } = await planAuthoredDocuments({
12
- base, basePath, config, variants, language, readerFilters, contentFilter, metadataProvider, metadataDependencies, sourceOverrides, authoringFiles, allowRouteCollisions
13
+ base, basePath, config, variants, language, readerFilters, contentFilter, featureContext: loaded.deployment?.context, metadataProvider, metadataDependencies, sourceOverrides, authoringFiles, allowRouteCollisions
13
14
  });
14
15
  if (config.snippets !== undefined && (!config.snippets || typeof config.snippets !== 'object' || Array.isArray(config.snippets)))
15
16
  throw new Error('snippets must be a mapping');
@@ -29,6 +30,20 @@ export async function prepareContentWorkspace({ base, basePath, config, loaded,
29
30
  config, loaded, variants, docs, knownDocumentIds, knownDocumentTypes, authoredOperationIds, routeOwners, basePath, readerFilters, contentFilter, allowRouteCollisions
30
31
  });
31
32
  const { api, sourceForVariant, knownOperations } = apiPlan;
33
+ for (const [key, doc] of docs) {
34
+ doc.featureContext = loaded.deployment?.context;
35
+ for (const operationId of doc.frontmatter.operations ?? []) {
36
+ const known = knownOperations.get(operationId);
37
+ const definition = known && apiPlan.apiDefinitions[known.apiId];
38
+ const operation = definition?.operations?.find(entry => entry.id === operationId);
39
+ if (!matchesFeatures(definition?.requires, loaded.deployment?.context, `API ${known?.apiId}`) || !matchesFeatures(operation?.requires, loaded.deployment?.context, `Operation ${operationId}`) || !matchesFeatures(apiPlan.operationRequirements.get(keyFor(operationId, doc.variant)), loaded.deployment?.context, `Operation ${operationId} (${doc.variant.version})`)) {
40
+ docs.delete(key);
41
+ const decision = loaded.deployment?.context.decisions.find(decision => decision.id === `${doc.file}: ${doc.frontmatter.id}`);
42
+ if (decision)
43
+ decision.included = false;
44
+ }
45
+ }
46
+ }
32
47
  const relatedApiTargets = new Map();
33
48
  const guidesByApi = new Map();
34
49
  for (const doc of docs.values()) {
@@ -57,6 +72,8 @@ export async function prepareContentWorkspace({ base, basePath, config, loaded,
57
72
  const operation = definition?.operations?.find(entry => entry.id === operationId);
58
73
  if (!definition || !operation)
59
74
  throw new Error(`unknown stable operation ${operationId}`);
75
+ if (!matchesFeatures(definition.requires, loaded.deployment?.context, `API ${known.apiId}`) || !matchesFeatures(operation.requires, loaded.deployment?.context, `Operation ${operationId}`) || !matchesFeatures(apiPlan.operationRequirements.get(keyFor(operationId, variant)), loaded.deployment?.context, `Operation ${operationId} (${variant.version})`) || (loaded.deployment?.context.grants && knownDocumentIds.has(known.reference) && !docs.has(keyFor(known.reference, variant))))
76
+ throw new Error(`Operation ${operationId} is excluded from this profile/publication`);
60
77
  if (!operation.sourceOperationId)
61
78
  throw new Error(`API ${known.apiId} operation needs sourceOperationId`);
62
79
  const binding = sourceForVariant(known.apiId, definition, variant);
@@ -0,0 +1,52 @@
1
+ export type FeatureRequirements = {
2
+ allOf?: string[];
3
+ anyOf?: string[];
4
+ };
5
+ export type FeatureContext = {
6
+ requireClassification?: boolean;
7
+ classificationPublications?: Array<{
8
+ channel: string;
9
+ version: string;
10
+ language: string;
11
+ }>;
12
+ features: Set<string>;
13
+ grants?: Set<string>;
14
+ digest: string;
15
+ pruneNavigation: boolean;
16
+ decisions: Array<{
17
+ kind: 'page' | 'section';
18
+ id: string;
19
+ included: boolean;
20
+ requires?: FeatureRequirements;
21
+ }>;
22
+ provenance: Record<string, string[]>;
23
+ };
24
+ export type ProfileResolution = {
25
+ context: FeatureContext;
26
+ overlay: Record<string, unknown>;
27
+ file?: string;
28
+ dependencies: string[];
29
+ assetFiles: string[];
30
+ digest: string;
31
+ };
32
+ /** Classification is an authored decision, never inferred from merged defaults. */
33
+ export declare function validateFeatureClassification(source: Record<string, unknown>, context: FeatureContext | undefined, label: string): void;
34
+ export declare function featureRequirements(value: unknown, context: FeatureContext | undefined, label: string): FeatureRequirements | undefined;
35
+ export declare function matchesFeatures(value: unknown, context: FeatureContext | undefined, label: string, kind?: 'page' | 'section'): boolean;
36
+ /** Validate every condition, but never resolve a dependency in an excluded branch. */
37
+ export declare function pruneFeatureTree(tree: any, context: FeatureContext | undefined, file: string): any;
38
+ /** Returned to the caller or written under .shoalrun, never a served artifact. */
39
+ export declare function profileSelection(resolution: ProfileResolution | undefined): {
40
+ schemaVersion: number;
41
+ profileDigest: string;
42
+ features: string[];
43
+ provenance: Record<string, string[]>;
44
+ decisions: {
45
+ kind: "page" | "section";
46
+ id: string;
47
+ included: boolean;
48
+ requires?: FeatureRequirements;
49
+ }[];
50
+ } | undefined;
51
+ /** A profile is data, never executable configuration or a directory to discover. */
52
+ export declare function resolveDeploymentProfile(settings: unknown, config: Record<string, any>, declaringFile: string, profileFile?: string): Promise<ProfileResolution | undefined>;
@@ -0,0 +1,277 @@
1
+ import { readFile, lstat, realpath } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { parseYaml } from './yaml-value.js';
5
+ import { defaultLanguage } from './frontmatter.js';
6
+ import { paletteCatalog, presetCatalog } from './palettes.js';
7
+ import { validateHeader, validateFooter } from './site-identity.js';
8
+ import { normalizeBasePath } from './site-path.js';
9
+ const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
10
+ const hash = (value) => createHash('sha256').update(value).digest('hex');
11
+ const id = (value) => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value);
12
+ function keys(value, allowed, label) {
13
+ if (!object(value))
14
+ throw new Error(`${label} must be a mapping`);
15
+ for (const key of Object.keys(value))
16
+ if (!allowed.includes(key))
17
+ throw new Error(`${label}: unsupported setting ${key}`);
18
+ }
19
+ function names(value, label, nonempty = false) {
20
+ if (!Array.isArray(value) || (nonempty && !value.length) || value.some(item => !id(item)))
21
+ throw new Error(`${label} must be ${nonempty ? 'a nonempty' : 'a'} list of feature IDs`);
22
+ return [...new Set(value)];
23
+ }
24
+ function profileData(value) {
25
+ if (!value || typeof value !== 'object')
26
+ return;
27
+ for (const [key, child] of Object.entries(value)) {
28
+ if (['__proto__', 'constructor', 'prototype'].includes(key))
29
+ throw new Error(`Deployment profile: unsupported data key ${key}`);
30
+ profileData(child);
31
+ }
32
+ }
33
+ /** Classification is an authored decision, never inferred from merged defaults. */
34
+ export function validateFeatureClassification(source, context, label) {
35
+ if (source.shared !== undefined && source.shared !== true)
36
+ throw new Error(`${label}: shared must be true; use requires for restricted content`);
37
+ if (source.shared === true && source.requires !== undefined)
38
+ throw new Error(`${label}: declare shared: true or requires, not both`);
39
+ if (context?.requireClassification && source.shared !== true && source.requires === undefined)
40
+ throw new Error(`${label}: unclassified content; declare shared: true or requires explicitly in this source (inherited defaults do not count)`);
41
+ if (source.requires !== undefined)
42
+ featureRequirements(source.requires, context, label);
43
+ }
44
+ export function featureRequirements(value, context, label) {
45
+ if (value === undefined)
46
+ return;
47
+ keys(value, ['allOf', 'anyOf'], `${label}: requires`);
48
+ if (!Object.keys(value).length)
49
+ throw new Error(`${label}: requires needs allOf or anyOf`);
50
+ if (!context)
51
+ throw new Error(`${label}: feature requirements need profiles.manifest and an explicit --profile`);
52
+ const result = {};
53
+ for (const clause of ['allOf', 'anyOf'])
54
+ if (value[clause] !== undefined) {
55
+ result[clause] = names(value[clause], `${label}: requires.${clause}`, true);
56
+ for (const name of result[clause])
57
+ if (!context.features.has(name))
58
+ throw new Error(`${label}: unknown feature ${name}`);
59
+ }
60
+ if (!context.grants)
61
+ throw new Error(`${label}: feature requirements need an explicit --profile (an empty feature list is valid)`);
62
+ return result;
63
+ }
64
+ export function matchesFeatures(value, context, label, kind = 'page') {
65
+ const requirement = featureRequirements(value, context, label);
66
+ const included = !requirement || ((!requirement.allOf || requirement.allOf.every(name => context.grants.has(name))) && (!requirement.anyOf || requirement.anyOf.some(name => context.grants.has(name))));
67
+ context?.decisions.push({ kind, id: label, included, ...(requirement ? { requires: requirement } : {}) });
68
+ return included;
69
+ }
70
+ /** Validate every condition, but never resolve a dependency in an excluded branch. */
71
+ export function pruneFeatureTree(tree, context, file) {
72
+ const walk = (node, parentIncluded = true) => {
73
+ if (!node || typeof node !== 'object')
74
+ return [node];
75
+ if (node.name === 'ForFeature') {
76
+ const label = `${file}:${node.position?.start?.line ?? 1}: <ForFeature>`;
77
+ const attributes = node.attributes ?? [];
78
+ if (node.type !== 'mdxJsxFlowElement' || !node.children?.length || !attributes.length || attributes.some((item) => item.type !== 'mdxJsxAttribute' || !['allOf', 'anyOf'].includes(item.name) || typeof item.value !== 'string') || new Set(attributes.map((item) => item.name)).size !== attributes.length)
79
+ throw new Error(`${label} needs block content and distinct literal allOf/anyOf attributes`);
80
+ const requirements = Object.fromEntries(attributes.map((item) => [item.name, item.value.trim().split(/\s+/).filter(Boolean)]));
81
+ const included = matchesFeatures(requirements, context, label, 'section');
82
+ if (!parentIncluded && context?.decisions.length)
83
+ context.decisions[context.decisions.length - 1].included = false;
84
+ const children = node.children.flatMap((child) => walk(child, parentIncluded && included));
85
+ return included && parentIncluded ? children : [];
86
+ }
87
+ return [{ ...node, ...(node.children ? { children: node.children.flatMap((child) => walk(child, parentIncluded)) } : {}) }];
88
+ };
89
+ return walk(tree)[0];
90
+ }
91
+ /** Returned to the caller or written under .shoalrun, never a served artifact. */
92
+ export function profileSelection(resolution) {
93
+ if (!resolution?.context.grants)
94
+ return;
95
+ return { schemaVersion: 1, profileDigest: resolution.digest, features: [...resolution.context.grants].sort(), provenance: resolution.context.provenance, decisions: resolution.context.decisions };
96
+ }
97
+ /** A profile is data, never executable configuration or a directory to discover. */
98
+ export async function resolveDeploymentProfile(settings, config, declaringFile, profileFile) {
99
+ if (settings === undefined) {
100
+ if (profileFile)
101
+ throw new Error('--profile requires profiles.manifest in the shared configuration');
102
+ return;
103
+ }
104
+ keys(settings, ['manifest', 'required', 'pruneNavigation', 'requireClassification'], 'profiles');
105
+ if (typeof settings.manifest !== 'string' || !settings.manifest.trim())
106
+ throw new Error('profiles.manifest must be a local path');
107
+ for (const setting of ['required', 'pruneNavigation', 'requireClassification'])
108
+ if (settings[setting] !== undefined && typeof settings[setting] !== 'boolean')
109
+ throw new Error(`profiles.${setting} must be boolean`);
110
+ const manifestFile = path.resolve(path.dirname(declaringFile), settings.manifest);
111
+ const manifestBytes = await readFile(manifestFile);
112
+ const manifest = parseYaml(manifestBytes.toString('utf8'), manifestFile);
113
+ keys(manifest, ['schemaVersion', 'features', 'skus'], 'SKU manifest');
114
+ if (manifest.schemaVersion !== 1 || !object(manifest.features) || !object(manifest.skus))
115
+ throw new Error('SKU manifest needs schemaVersion: 1, features, and skus mappings');
116
+ const features = new Set(Object.keys(manifest.features));
117
+ for (const [name, definition] of Object.entries(manifest.features)) {
118
+ if (!id(name))
119
+ throw new Error(`SKU manifest: invalid feature ID ${name}`);
120
+ keys(definition, ['description', 'lifecycle'], `Feature ${name}`);
121
+ if (definition.description !== undefined && typeof definition.description !== 'string')
122
+ throw new Error(`Feature ${name}: description must be text`);
123
+ if (definition.lifecycle !== undefined && !['active', 'deprecated', 'retired'].includes(definition.lifecycle))
124
+ throw new Error(`Feature ${name}: invalid lifecycle`);
125
+ }
126
+ for (const [sku, definition] of Object.entries(manifest.skus)) {
127
+ if (!id(sku))
128
+ throw new Error(`SKU manifest: invalid SKU ID ${sku}`);
129
+ keys(definition, ['revisions'], `SKU ${sku}`);
130
+ if (!object(definition.revisions) || !Object.keys(definition.revisions).length)
131
+ throw new Error(`SKU ${sku}: revisions must be a nonempty mapping`);
132
+ for (const [revision, grant] of Object.entries(definition.revisions)) {
133
+ if (!id(revision))
134
+ throw new Error(`SKU ${sku}: invalid revision ${revision}`);
135
+ keys(grant, ['features'], `SKU ${sku}@${revision}`);
136
+ for (const feature of names(grant.features, `SKU ${sku}@${revision}.features`))
137
+ if (!features.has(feature))
138
+ throw new Error(`SKU ${sku}@${revision}: unknown feature ${feature}`);
139
+ }
140
+ }
141
+ const digest = hash(manifestBytes);
142
+ const context = { features, digest, pruneNavigation: settings.pruneNavigation ?? false, decisions: [], provenance: Object.create(null) };
143
+ context.requireClassification = settings.requireClassification ?? false;
144
+ if (context.requireClassification)
145
+ context.classificationPublications = (config.publications?.enabled ?? []).map((entry) => ({ channel: entry.channel, version: entry.version, language: entry.language ?? defaultLanguage(config) }));
146
+ if (!profileFile) {
147
+ if (settings.required !== false)
148
+ throw new Error('This project requires an explicit --profile FILE');
149
+ return { context, overlay: {}, dependencies: [manifestFile], assetFiles: [], digest };
150
+ }
151
+ const file = await realpath(path.resolve(profileFile));
152
+ const bytes = await readFile(file);
153
+ const profile = parseYaml(bytes.toString('utf8'), file);
154
+ profileData(profile);
155
+ keys(profile, ['schemaVersion', 'skus', 'features', 'site', 'theme', 'variables', 'publications'], 'Deployment profile');
156
+ if (profile.schemaVersion !== 1)
157
+ throw new Error('Deployment profile needs schemaVersion: 1');
158
+ if (!Object.hasOwn(profile, 'skus') && !Object.hasOwn(profile, 'features'))
159
+ throw new Error('Deployment profile must declare skus or features explicitly (empty lists are valid)');
160
+ const grants = new Set();
161
+ const add = (feature, reason) => {
162
+ if (!features.has(feature))
163
+ throw new Error(`Deployment profile: unknown feature ${feature}`);
164
+ grants.add(feature);
165
+ (context.provenance[feature] ??= []).push(reason);
166
+ };
167
+ if (profile.skus !== undefined) {
168
+ if (!Array.isArray(profile.skus))
169
+ throw new Error('Deployment profile skus must be a list');
170
+ for (const purchase of profile.skus) {
171
+ keys(purchase, ['id', 'revision'], 'Profile SKU');
172
+ if (!id(purchase.id) || !id(purchase.revision))
173
+ throw new Error('Profile SKU needs explicit id and revision strings');
174
+ const definition = manifest.skus[purchase.id];
175
+ if (!Object.hasOwn(manifest.skus, purchase.id) || !Object.hasOwn(definition.revisions, purchase.revision))
176
+ throw new Error(`Deployment profile: unknown SKU revision ${purchase.id}@${purchase.revision}`);
177
+ for (const feature of definition.revisions[purchase.revision].features)
178
+ add(feature, `sku:${purchase.id}@${purchase.revision}`);
179
+ }
180
+ }
181
+ for (const feature of names(profile.features === undefined ? [] : profile.features, 'Deployment profile features'))
182
+ add(feature, 'direct');
183
+ context.grants = grants;
184
+ context.digest = hash(JSON.stringify([digest, hash(bytes)]));
185
+ const overlay = {};
186
+ const assetFiles = [];
187
+ if (profile.site !== undefined) {
188
+ keys(profile.site, ['title', 'origin', 'basePath', 'favicon', 'logo', 'header', 'footer', 'indexing'], 'Profile site');
189
+ if (profile.site.title !== undefined && (typeof profile.site.title !== 'string' || !profile.site.title.trim()))
190
+ throw new Error('Profile site.title must be nonempty text');
191
+ if (profile.site.indexing !== undefined && typeof profile.site.indexing !== 'boolean')
192
+ throw new Error('Profile site.indexing must be boolean');
193
+ if (profile.site.basePath !== undefined)
194
+ normalizeBasePath(profile.site.basePath);
195
+ if (profile.site.origin !== undefined) {
196
+ let valid = false;
197
+ try {
198
+ const origin = new URL(profile.site.origin);
199
+ valid = origin.protocol === 'https:' && origin.origin === profile.site.origin;
200
+ }
201
+ catch { /* Report the input contract below. */ }
202
+ if (!valid)
203
+ throw new Error('Profile site.origin must be an HTTPS origin without a path');
204
+ }
205
+ if (profile.site.header !== undefined) {
206
+ keys(profile.site.header, ['links'], 'Profile site.header');
207
+ validateHeader({ ...(object(config.site?.header) ? config.site.header : {}), ...profile.site.header });
208
+ }
209
+ if (profile.site.footer !== undefined) {
210
+ keys(profile.site.footer, ['columns', 'links', 'text'], 'Profile site.footer');
211
+ validateFooter({ ...(object(config.site?.footer) ? config.site.footer : {}), ...profile.site.footer });
212
+ }
213
+ overlay.site = profile.site;
214
+ for (const setting of ['favicon', 'logo'])
215
+ if (profile.site[setting] !== undefined) {
216
+ const asset = profile.site[setting];
217
+ if (typeof asset !== 'string' || path.isAbsolute(asset) || !/\.(svg|png)$/i.test(asset))
218
+ throw new Error(`Profile site.${setting} must be a relative .svg or .png path`);
219
+ const target = path.resolve(path.dirname(file), asset);
220
+ if (!target.startsWith(path.dirname(file) + path.sep))
221
+ throw new Error('Profile asset must stay within the profile directory');
222
+ // Explicit assets only. Symlinks must not introduce an undeclared capture root.
223
+ let current = target;
224
+ while (current !== path.dirname(file)) {
225
+ if ((await lstat(current)).isSymbolicLink())
226
+ throw new Error('Profile assets must not use symlinks');
227
+ current = path.dirname(current);
228
+ }
229
+ if (!(await lstat(target)).isFile())
230
+ throw new Error(`Profile ${setting} must be a regular file`);
231
+ assetFiles.push(target);
232
+ }
233
+ }
234
+ if (profile.theme !== undefined) {
235
+ keys(profile.theme, ['preset', 'palette', 'mode', 'toggle'], 'Profile theme');
236
+ // File-backed palettes/fonts and CSS require a separate dependency contract.
237
+ if (profile.theme.palette !== undefined && !paletteCatalog.some(entry => entry.name === profile.theme.palette))
238
+ throw new Error('Profile theme.palette must name a built-in palette');
239
+ if (profile.theme.preset !== undefined && !presetCatalog.some(entry => entry.name === profile.theme.preset))
240
+ throw new Error('Profile theme.preset must name a built-in preset');
241
+ if (profile.theme.mode !== undefined && !['light', 'dark', 'system'].includes(profile.theme.mode))
242
+ throw new Error('Profile theme.mode must be light, dark, or system');
243
+ if (profile.theme.toggle !== undefined && typeof profile.theme.toggle !== 'boolean')
244
+ throw new Error('Profile theme.toggle must be boolean');
245
+ const mode = profile.theme.mode ?? config.theme?.mode ?? 'light';
246
+ const toggle = profile.theme.toggle ?? config.theme?.toggle ?? (mode === 'system');
247
+ if (toggle && mode !== 'system')
248
+ throw new Error('Profile theme.toggle needs effective theme.mode: system');
249
+ overlay.theme = profile.theme;
250
+ }
251
+ if (profile.variables !== undefined) {
252
+ if (!object(profile.variables) || Object.entries(profile.variables).some(([key, value]) => !id(key) || (!['string', 'number', 'boolean'].includes(typeof value) || typeof value === 'number' && !Number.isFinite(value))))
253
+ throw new Error('Profile variables must map IDs to string, number, or boolean values');
254
+ overlay.variables = profile.variables;
255
+ }
256
+ if (profile.publications !== undefined) {
257
+ keys(profile.publications, ['enabled'], 'Profile publications');
258
+ const enabled = profile.publications.enabled;
259
+ const language = defaultLanguage(config);
260
+ const coordinate = (value) => JSON.stringify([value.channel, value.version, value.language ?? language]);
261
+ if (!Array.isArray(enabled) || !enabled.length)
262
+ throw new Error('Profile publications.enabled must be a nonempty subset of shared publications.enabled');
263
+ const selected = new Set();
264
+ for (const variant of enabled) {
265
+ keys(variant, ['channel', 'version', 'language'], 'Profile publication');
266
+ if (typeof variant.channel !== 'string' || typeof variant.version !== 'string' || (variant.language !== undefined && typeof variant.language !== 'string') || !config.publications?.enabled?.some((entry) => coordinate(entry) === coordinate(variant)))
267
+ throw new Error('Profile publication is not enabled by the shared configuration');
268
+ const key = coordinate(variant);
269
+ if (selected.has(key))
270
+ throw new Error('Profile publications.enabled must not repeat publication coordinates');
271
+ selected.add(key);
272
+ }
273
+ overlay.publications = profile.publications;
274
+ }
275
+ context.digest = hash(JSON.stringify([digest, hash(bytes), await Promise.all(assetFiles.map(async (asset) => hash(await readFile(asset))))]));
276
+ return { context, overlay, file, dependencies: [manifestFile, file, ...assetFiles], assetFiles, digest: context.digest };
277
+ }
@@ -4,7 +4,7 @@ import { validateCardMetadata } from './cards.js';
4
4
  import { resolveTaxonomy, validateTaxonomy, taxonomySchema } from './taxonomy.js';
5
5
  import { validateStandaloneSectionDeclarations } from './section-identity.js';
6
6
  export const nativePublicFields = new Set(['id', 'title', 'type', 'summary', 'description', 'language', 'locale', 'date', 'updated', 'verified', 'releaseVersion', 'product', 'area', 'subarea', 'operations', 'relatedApiReferences', 'audiences', 'readerFilters', 'tags', 'subjectLifecycle', 'subjectDeprecation', 'change', 'lifecycle']);
7
- export const protectedMetadataRoots = new Set(['standaloneSections', 'access', 'seo', 'markdownMetadata', 'variables', 'tocDepth', 'layout', 'showNavigationTabs', 'showVerified', 'image', 'private', 'internal', 'card', 'displayText', 'publications']);
7
+ export const protectedMetadataRoots = new Set(['shared', 'requires', 'standaloneSections', 'access', 'seo', 'markdownMetadata', 'variables', 'tocDepth', 'layout', 'showNavigationTabs', 'showVerified', 'image', 'private', 'internal', 'card', 'displayText', 'publications']);
8
8
  const fieldTypes = new Set(['string', 'number', 'integer', 'boolean', 'object', 'array']);
9
9
  const safePath = (value) => /^[A-Za-z_][A-Za-z0-9_-]*(?:\.[A-Za-z_][A-Za-z0-9_-]*)*$/.test(value);
10
10
  const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
@@ -33,7 +33,7 @@ export function frontmatterLayers(config = {}) {
33
33
  const layer = config[name] === undefined ? {} : config[name];
34
34
  if (!object(layer))
35
35
  throw new Error(`frontmatter.${name} must be a mapping`);
36
- for (const field of ['id', 'slug', 'publications', 'standaloneSections', 'subjectDeprecation', 'change'])
36
+ for (const field of ['id', 'slug', 'shared', 'publications', 'standaloneSections', 'subjectDeprecation', 'change'])
37
37
  if (Object.hasOwn(layer, field))
38
38
  throw new Error(`frontmatter.${name}.${field} must stay page-specific`);
39
39
  layers[name] = mergeMetadata({}, layer);
@@ -114,6 +114,10 @@ function matchesType(value, type) {
114
114
  export function validateFrontmatter(frontmatter, config, file, source) {
115
115
  const taxonomy = resolveTaxonomy(config?.taxonomy);
116
116
  validateTaxonomy(frontmatter, taxonomy, file, source);
117
+ if (frontmatter.shared !== undefined && frontmatter.shared !== true)
118
+ throw new Error(`${file}: shared must be true; use requires for restricted content`);
119
+ if (frontmatter.shared === true && frontmatter.requires !== undefined)
120
+ throw new Error(`${file}: declare shared: true or requires, not both`);
117
121
  validateCardMetadata(frontmatter.card, file);
118
122
  validateSubjectDeprecation(frontmatter.subjectDeprecation, file);
119
123
  validateAuthoredChange(frontmatter, file);
@@ -239,6 +243,8 @@ export function frontmatterSchema(config) {
239
243
  operations: { type: 'array', items: { type: 'string' }, description: 'Stable API operation IDs that connect this page to endpoint references and related documents.' },
240
244
  relatedApiReferences: { type: 'array', items: { type: 'string' }, description: 'API reference page IDs linked automatically from this guide and back from each reference, within the same publication.' },
241
245
  audiences: { type: 'array', items: { type: 'string' }, description: 'Readers this page is intended for; does not control who can access it.' },
246
+ shared: { type: 'boolean', const: true, description: 'Explicitly classify this page as shared across customer editions. Page-specific; cannot combine with requires. Sections may still use ForFeature.' },
247
+ requires: { type: 'object', minProperties: 1, additionalProperties: false, properties: Object.fromEntries(['allOf', 'anyOf'].map(name => [name, { type: 'array', minItems: 1, items: { type: 'string', minLength: 1 } }])), description: 'Build-time feature requirements from the shared SKU manifest. Both clauses must pass when present; omitted means shared within existing publication/access eligibility. Requires an explicit deployment profile.' },
242
248
  readerFilters: { type: 'object', additionalProperties: { type: 'array', items: { type: 'string' } }, description: 'Configured reader filter choices for this page. Missing facets are shared.' },
243
249
  tags: { type: 'array', items: { type: 'string' }, description: 'Editorial labels used to select pages in document collections.' },
244
250
  change: { type: 'object', additionalProperties: false, required: ['kind', 'affects'], description: 'Page-specific authored change meaning on a changelog-entry: kind, affected document IDs, and optional migration guide ID in the same publication. Omission means unclassified, not nonbreaking.', properties: { kind: { type: 'string', enum: [...changeKinds] }, affects: { type: 'array', minItems: 1, uniqueItems: true, items: { type: 'string', minLength: 1 } }, migration: { type: 'string', minLength: 1 } } },
@@ -310,18 +316,19 @@ export function frontmatterReference(config, sectionsOnly = false) {
310
316
  'This generated file is a project-wide field guide for authors and agents working with the repository. The build writes it to `.shoalrun/frontmatter-reference.md` beside the project config. Link to that local file from repository instructions when agents need it.', '',
311
317
  'Frontmatter is the YAML header between the first pair of `---` lines in a `.md` or `.mdx` source page. It tells the compiler what the page is, where to publish it, and how to connect it to other content. The prose below the second `---` is the page body. This example header illustrates the fields; it is not necessarily the header of the page linking here.', '',
312
318
  '```yaml', '---', 'id: guide.create-payment', 'title: Create a payment', 'summary: Submit a payment with the example API.', 'type: how-to', 'slug: /guides/create-payment', 'channel: production', 'version: v1', 'access: { policy: public }', '---', '```', '',
313
- 'This header describes one page. Its `id` stays stable when `slug` changes; `channel` and `version` select the publication in which that page appears. A **publication** is an enabled combination of channel, version, and language in project config. A public page also needs `access.policy: public`. The “Needed” column below refers to the *resolved* header after project defaults and enforced values are merged. Only `id` and `slug` must be written on each source page.', '',
319
+ 'This header describes one page. Its `id` stays stable when `slug` changes; `channel` and `version` select the publication in which that page appears. A **publication** is an enabled combination of channel, version, and language in project config. A public page also needs `access.policy: public`. The “Needed” column below refers to the *resolved* header after project defaults and enforced values are merged. IDs and slugs must be written on each source page; optional explicit-classification and taxonomy policies can require additional source fields.', '',
314
320
  '## Identify and route a page', '',
315
321
  'Use the page ID for links and cards that should survive a URL change. The slug supplies the page part of the URL; publication prefixes may be added around it. Summary is short preview text; description is a separate, optional fuller account. Project output rules choose which field supplies each surface.', '',
316
322
  ...table, ...nativeRows(['id', 'title', 'summary', 'description', 'type', 'slug']), '',
317
323
  '## Choose a publication and visibility', '',
318
324
  'The project enables specific channel/version/language combinations. A page is built only for combinations matching its resolved values. `audiences` and `readerFilters` select reader views when the site config enables them; `access.policy` determines whether the page enters this public build. A draft page is excluded.', '',
319
- ...table, ...nativeRows(['channel', 'version', 'language', 'locale', 'access', 'audiences', 'readerFilters']), '',
325
+ ...table, ...nativeRows(['channel', 'version', 'language', 'locale', 'access', 'shared', 'requires', 'audiences', 'readerFilters']), '',
320
326
  'Language is the content language and selects a publication. Locale describes formatting conventions in the content and does not select a publication. `frontmatter.locales` can set a project locale for each language; pages may override it.', '',
321
327
  '## Categorize, connect, and present the page', '',
322
328
  'By default, `product`, `area`, and `subarea` are optional, nonempty string categories. They can come from project defaults and appear in the document manifest and search index. A matching configured reader facet makes each category a page filter; without that facet it remains taxonomy only. `operations` links a guide or reference to stable API operation IDs. On a guide or how-to page, `relatedApiReferences` names API reference page IDs to add cross links in both directions within the same publication. Tags help document collections find the page. Card settings change its preview when another page links to it; they do not change the destination. `subjectLifecycle` displays the status of the documented API or feature; `lifecycle` is available for the document\'s editorial state. `tocDepth` limits the HTML page table of contents; use 0 to hide it. `layout` picks the HTML page layout: `docs`, `wide`, `landing`, or `splash`. `showNavigationTabs` overrides the navigation tab bar for this page; landing and splash pages hide it by default. `image` replaces the generated link-preview image. `date` and `releaseVersion` can identify a changelog entry under the project\'s `frontmatter.changelogIdentifier` rule.', '',
323
329
  ...table, ...nativeRows(['product', 'area', 'subarea', 'operations', 'relatedApiReferences', 'tags', 'date', 'releaseVersion', 'subjectLifecycle', 'subjectDeprecation', 'change', 'lifecycle', 'card', 'standaloneSections', 'tocDepth', 'layout', 'showNavigationTabs', 'showVerified', 'image']), '',
324
330
  'Projects can opt into built-in category enforcement with `frontmatter.taxonomy`. It declares areas and their allowed subareas, optional required fields, and optional explicit-field requirements for authored pages. Its `multiple` list permits arrays for selected category fields; otherwise each field allows one value. Defaults and enforced values still apply; generated API references use resolved categories. Definitions appear in the document manifest and constrain the generated frontmatter schema. Omit taxonomy or set it to false to preserve optional category behavior.', '',
331
+ 'With `profiles.requireClassification: true`, every public source page in the shared enabled publications needs an explicit `shared: true` or `requires` declaration before customer selection. Defaults do not count. Shared pages may contain feature-specific sections wrapped in `<ForFeature>`; unwrapped text remains shared.', '',
325
332
  '## Control output details', '',
326
333
  '`seo` changes the browser title or HTML meta description. `displayText` overrides the text chosen for one output surface on this page. `variables` supplies values to registered snippets before they expand into the page body. `markdownMetadata` selects or overrides the YAML preamble of the generated Markdown twin—the machine-readable copy of this page. That generated preamble is separate from the source frontmatter shown above.', '',
327
334
  ...table, ...nativeRows(['seo', 'displayText', 'variables', 'markdownMetadata']), '',
@@ -29,6 +29,8 @@ export async function materializePages({ variables, frontmatterConfig, publicati
29
29
  local.set(other.frontmatter.id, other);
30
30
  const parsed = parse.parse(doc.body);
31
31
  const tree = await resolveApiEmbedTree(transformMdx(parsed, doc, local, snippets, variables, snippetUses, calloutTypes, base, readerFilters, frontmatterConfig, publications, badgeVariants, componentPlugins), api, operationId => sourceForOperation(operationId, doc.variant), apiId => {
32
+ if (doc.featureContext?.grants)
33
+ throw new Error('Standalone API schema embeds require an explicitly scoped source and are not supported in profiled builds; select an operation instead');
32
34
  const definition = apiPlan.apiDefinitions[apiId];
33
35
  if (!definition)
34
36
  throw new Error(`unknown API ${apiId}`);
@@ -42,6 +42,7 @@ export type PageShell = {
42
42
  generated: boolean;
43
43
  };
44
44
  favicon?: string;
45
+ logo?: string;
45
46
  touchIcon?: boolean;
46
47
  origin?: string;
47
48
  indexing?: boolean;
@@ -62,6 +62,8 @@ export function buildHtmlPage(doc, tree, markdownUrl, siteTitle, navigation, exa
62
62
  const audienceLabels = doc.frontmatter.audiences?.filter(label => label.trim()) ?? [];
63
63
  const audienceBadges = audienceLabels.length ? `<div class="audience-labels" role="group" aria-label="Intended audience"><span class="audience-label-heading">For</span>${audienceLabels.map(label => `<span class="audience-badge">${esc(readerConfig?.facets.audience?.options[label] ?? label)}</span>`).join('')}</div>` : '';
64
64
  const title = doc.frontmatter.seo?.title ?? doc.frontmatter.title;
65
+ const brand = `${shell.logo ? `<img class="brand-logo" src="${esc(shell.logo)}" alt="">` : ''}${esc(siteTitle)}`;
66
+ const brandClass = shell.logo ? 'brand brand-custom' : 'brand';
65
67
  const pageTitle = String(title).trim();
66
68
  const brandedTitle = `${pageTitle} | ${siteTitle.trim()}`;
67
69
  const socialTitle = !doc.frontmatter.seo?.title && pageTitle.length < 30 && brandedTitle.length <= 70 ? brandedTitle : pageTitle;
@@ -174,6 +176,6 @@ export function buildHtmlPage(doc, tree, markdownUrl, siteTitle, navigation, exa
174
176
  const menu = side || sectionNavigation ? `<a class="mobile-menu" href="#${side ? 'docs-navigation' : 'docs-sections'}">Menu</a>` : '';
175
177
  const rail = toc || panel ? `<aside class="page-rail">${doc.endpoint ? `${panel}${toc}` : `${toc}${panel}`}</aside>` : '';
176
178
  const breadcrumbs = (shell.breadcrumbs?.length ?? 0) > 1 && !hero ? `<nav class="doc-breadcrumbs" aria-label="Breadcrumb"><ol>${shell.breadcrumbs.map((item, index) => `<li>${index === shell.breadcrumbs.length - 1 ? `<span aria-current="page">${esc(item.label)}</span>` : item.url ? `<a href="${esc(item.url)}">${esc(item.label)}</a>` : `<span>${esc(item.label)}</span>`}</li>`).join('')}</ol></nav>` : '';
177
- const markup = `<!doctype html><html lang="${esc(doc.variant.language)}"${shell.themeCss || shell.paletteCss ? ` data-theme="${esc(shell.preset ?? 'shoal')}" data-palette="${esc(shell.palette ?? '')}"` : ''}${shell.masthead ? ` data-masthead="${esc(shell.masthead)}"` : ''}${layout === 'docs' ? '' : ` data-layout="${layout}"`}><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="color-scheme" content="${shell.mode === 'system' ? 'light dark' : shell.mode ?? 'light'}">${shell.indexing === false || doc.frontmatter.seo?.noindex === true ? '<meta name="robots" content="noindex">' : ''}${shell.origin ? `<link rel="canonical" href="${esc(absolute(doc.route))}">` : ''}${shell.toggle ? '<script src="/assets/mode.js"></script>' : ''}<title>${esc(String(title))}</title>${description ? `<meta name="description" content="${esc(String(description))}">` : ''}${socialDescription ? `<meta property="og:description" content="${esc(socialDescription)}">` : ''}${social}<link rel="alternate" type="text/markdown" href="${esc(markdownUrl)}"><link rel="describedby" href="/llms.min.txt" title="Documentation index">${icons}${shell.preloadFont ? `<link rel="preload" href="/assets/fonts/${esc(shell.preloadFont)}" as="font" type="font/woff2" crossorigin>` : ''}<link rel="stylesheet" href="/assets/site.css">${article.includes('data-theme-preview') ? '<link rel="stylesheet" href="/assets/theme-preview.css">' : ''}${shell.themeCss ? '<link rel="stylesheet" href="/assets/theme.css" data-theme-link="theme">' : ''}${shell.paletteCss ? '<link rel="stylesheet" href="/assets/palette.css" data-theme-link="palette">' : ''}${shell.badgesCss ? '<link rel="stylesheet" href="/assets/badges.css">' : ''}${shell.themeSwitcher ? `<script src="/assets/theme-switcher.js" data-themes="${esc(shell.themeSwitcher)}"></script>` : ''}${shell.fontsCss ? '<link rel="stylesheet" href="/assets/fonts.css">' : ''}${customCss ? '<link rel="stylesheet" href="/assets/project.css">' : ''}</head><body><a class="skip" href="#main">Skip to content</a>${agent.mode === 'disclosure' ? '' : agentBlock}<header class="mast"><div class="mast-inner">${shell.home ? `<a class="brand" href="/">${esc(siteTitle)}</a>` : `<span class="brand">${esc(siteTitle)}</span>`}${editionLabel && (shell.publicationAlternates?.length ?? 0) < 2 ? `<span class="edition">${esc(editionLabel)}</span>` : ''}${menu}${headerTools(shell, doc.variant, esc, readerConfig)}${agent.mode === 'disclosure' ? agentBlock : ''}</div></header>${sectionNavigation}${shortcuts}<div class="layout${layout === 'docs' ? '' : ` layout-${layout}`}${rail ? ' has-rail' : ''}${panel ? ' has-examples' : ''}${doc.endpoint ? ' api-reference' : ''}"><main id="main" class="content" tabindex="-1" data-document-type="${esc(doc.frontmatter.type)}"${readerConfig ? ` data-reader-page="${esc(readerFilterAttribute(doc.readerFilters ?? {}))}"` : ''}>${breadcrumbs}${hero ? '' : `<header class="page-head"><span class="eyebrow">${esc(typeLabel(doc.frontmatter.type))}</span>${doc.endpoint ? '' : badge(doc.frontmatter.subjectLifecycle)}<h1>${esc(doc.frontmatter.title)}${doc.endpoint ? badge(doc.frontmatter.subjectLifecycle) : ''}</h1>${pageHeader && !routeSummary ? `<p class="summary">${esc(pageHeader)}</p>` : ''}${pageDates}${endpoint}${audienceBadges}</header>`}${hero ? '' : `<div class="page-formats"><a class="formats" href="${esc(markdownUrl)}">Read as Markdown</a><button type="button" class="formats" data-copy-markdown hidden>Copy as Markdown</button><span role="status" data-copy-markdown-status></span></div>`}${toc ? '<div class="mobile-outline-slot"></div>' : ''}${jumps}${readerBanner}<article>${article}</article></main>${rail}${side}</div>${footer}${analyticsBootstrap}${readerBootstrap}<script type="module" src="/assets/copy-markdown.js"></script><script type="module" src="/assets/navigation.js"></script><script type="module" src="/assets/reader.js"></script><script type="module" src="/assets/tabs.js"></script>${shell.search ? '<script type="module" src="/assets/search.js"></script>' : ''}${article.includes('data-tooltip') ? '<script type="module" src="/assets/tooltips.js"></script>' : ''}${article.includes('data-component-module') ? '<script type="module" src="/assets/components.js"></script>' : ''}${article.includes('data-file-tree') ? '<script type="module" src="/assets/file-tree.js"></script>' : ''}${article.includes('data-image-expand') ? '<script type="module" src="/assets/image-viewer.js"></script>' : ''}${article.includes('data-table-filter') || article.includes('data-definition-filter') ? '<script type="module" src="/assets/table-filter.js"></script>' : ''}${article.includes('data-walkthrough') ? '<script type="module" src="/assets/walkthrough.js"></script>' : ''}${article.includes('data-cookbook-chooser') ? '<script type="module" src="/assets/cookbook-chooser.js"></script>' : ''}${article.includes('data-theme-preview') ? '<script type="module" src="/assets/theme-preview.js"></script>' : ''}${panel ? '<script type="module" src="/assets/example-panel.js"></script>' : ''}${panel.includes('data-code-samples') ? '<script type="module" src="/assets/code-samples.js"></script>' : ''}</body></html>`;
179
+ const markup = `<!doctype html><html lang="${esc(doc.variant.language)}"${shell.themeCss || shell.paletteCss ? ` data-theme="${esc(shell.preset ?? 'shoal')}" data-palette="${esc(shell.palette ?? '')}"` : ''}${shell.masthead ? ` data-masthead="${esc(shell.masthead)}"` : ''}${layout === 'docs' ? '' : ` data-layout="${layout}"`}><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="color-scheme" content="${shell.mode === 'system' ? 'light dark' : shell.mode ?? 'light'}">${shell.indexing === false || doc.frontmatter.seo?.noindex === true ? '<meta name="robots" content="noindex">' : ''}${shell.origin ? `<link rel="canonical" href="${esc(absolute(doc.route))}">` : ''}${shell.toggle ? '<script src="/assets/mode.js"></script>' : ''}<title>${esc(String(title))}</title>${description ? `<meta name="description" content="${esc(String(description))}">` : ''}${socialDescription ? `<meta property="og:description" content="${esc(socialDescription)}">` : ''}${social}<link rel="alternate" type="text/markdown" href="${esc(markdownUrl)}"><link rel="describedby" href="/llms.min.txt" title="Documentation index">${icons}${shell.preloadFont ? `<link rel="preload" href="/assets/fonts/${esc(shell.preloadFont)}" as="font" type="font/woff2" crossorigin>` : ''}<link rel="stylesheet" href="/assets/site.css">${article.includes('data-theme-preview') ? '<link rel="stylesheet" href="/assets/theme-preview.css">' : ''}${shell.themeCss ? '<link rel="stylesheet" href="/assets/theme.css" data-theme-link="theme">' : ''}${shell.paletteCss ? '<link rel="stylesheet" href="/assets/palette.css" data-theme-link="palette">' : ''}${shell.badgesCss ? '<link rel="stylesheet" href="/assets/badges.css">' : ''}${shell.themeSwitcher ? `<script src="/assets/theme-switcher.js" data-themes="${esc(shell.themeSwitcher)}"></script>` : ''}${shell.fontsCss ? '<link rel="stylesheet" href="/assets/fonts.css">' : ''}${customCss ? '<link rel="stylesheet" href="/assets/project.css">' : ''}</head><body><a class="skip" href="#main">Skip to content</a>${agent.mode === 'disclosure' ? '' : agentBlock}<header class="mast"><div class="mast-inner">${shell.home ? `<a class="${brandClass}" href="/">${brand}</a>` : `<span class="${brandClass}">${brand}</span>`}${editionLabel && (shell.publicationAlternates?.length ?? 0) < 2 ? `<span class="edition">${esc(editionLabel)}</span>` : ''}${menu}${headerTools(shell, doc.variant, esc, readerConfig)}${agent.mode === 'disclosure' ? agentBlock : ''}</div></header>${sectionNavigation}${shortcuts}<div class="layout${layout === 'docs' ? '' : ` layout-${layout}`}${rail ? ' has-rail' : ''}${panel ? ' has-examples' : ''}${doc.endpoint ? ' api-reference' : ''}"><main id="main" class="content" tabindex="-1" data-document-type="${esc(doc.frontmatter.type)}"${readerConfig ? ` data-reader-page="${esc(readerFilterAttribute(doc.readerFilters ?? {}))}"` : ''}>${breadcrumbs}${hero ? '' : `<header class="page-head"><span class="eyebrow">${esc(typeLabel(doc.frontmatter.type))}</span>${doc.endpoint ? '' : badge(doc.frontmatter.subjectLifecycle)}<h1>${esc(doc.frontmatter.title)}${doc.endpoint ? badge(doc.frontmatter.subjectLifecycle) : ''}</h1>${pageHeader && !routeSummary ? `<p class="summary">${esc(pageHeader)}</p>` : ''}${pageDates}${endpoint}${audienceBadges}</header>`}${hero ? '' : `<div class="page-formats"><a class="formats" href="${esc(markdownUrl)}">Read as Markdown</a><button type="button" class="formats" data-copy-markdown hidden>Copy as Markdown</button><span role="status" data-copy-markdown-status></span></div>`}${toc ? '<div class="mobile-outline-slot"></div>' : ''}${jumps}${readerBanner}<article>${article}</article></main>${rail}${side}</div>${footer}${analyticsBootstrap}${readerBootstrap}<script type="module" src="/assets/copy-markdown.js"></script><script type="module" src="/assets/navigation.js"></script><script type="module" src="/assets/reader.js"></script><script type="module" src="/assets/tabs.js"></script>${shell.search ? '<script type="module" src="/assets/search.js"></script>' : ''}${article.includes('data-tooltip') ? '<script type="module" src="/assets/tooltips.js"></script>' : ''}${article.includes('data-component-module') ? '<script type="module" src="/assets/components.js"></script>' : ''}${article.includes('data-file-tree') ? '<script type="module" src="/assets/file-tree.js"></script>' : ''}${article.includes('data-image-expand') ? '<script type="module" src="/assets/image-viewer.js"></script>' : ''}${article.includes('data-table-filter') || article.includes('data-definition-filter') ? '<script type="module" src="/assets/table-filter.js"></script>' : ''}${article.includes('data-walkthrough') ? '<script type="module" src="/assets/walkthrough.js"></script>' : ''}${article.includes('data-cookbook-chooser') ? '<script type="module" src="/assets/cookbook-chooser.js"></script>' : ''}${article.includes('data-theme-preview') ? '<script type="module" src="/assets/theme-preview.js"></script>' : ''}${panel ? '<script type="module" src="/assets/example-panel.js"></script>' : ''}${panel.includes('data-code-samples') ? '<script type="module" src="/assets/code-samples.js"></script>' : ''}</body></html>`;
178
180
  return basePath ? markup.replace(/((?:href|src)=")\/(assets\/|llms(?:\.min)?\.txt|apple-touch-icon\.png)/g, `$1${basePath}/$2`).replace('href="/"', `href="${basePath}/"`) : markup;
179
181
  }