@shoalrun/cli 0.1.0-alpha.14 → 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 +23 -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 +2 -0
  90. package/agent-docs/publish/content-export.sections/upgrade-an-existing-integration.md +0 -34
@@ -13,7 +13,7 @@ import { auditReference } from './reference-audit.js';
13
13
  import { iconLicenseFile } from './icons.js';
14
14
  import { parseAuditSettings, runAuditTest } from './audit.js';
15
15
  import { configFindingLocation, validationFinding } from './authoring-findings.js';
16
- import { authoringArtifactPath, captureSnapshotInputs, collectSnapshotInputs, confirmSnapshotInputs, excludedNames, isWithin, parseAuthoringBaseline, relativeInput, sha256, SnapshotError, validatorInputs } from './authoring-snapshot.js';
16
+ import { authoringArtifactPath, captureSnapshotInputs, collectSnapshotInputs, confirmSnapshotInputs, excludedNames, isWithin, parseAuthoringBaseline, relativeInput, sha256, SnapshotError, validatorInputs, validateProfileCapture, readProfileInput } from './authoring-snapshot.js';
17
17
  const framework = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
18
18
  // Include ordinary hoisted dependency locations in installed npm packages too.
19
19
  const checkModulePaths = (createRequire(import.meta.url).resolve.paths('yaml') ?? []).filter(directory => path.basename(directory) === 'node_modules').join(path.delimiter);
@@ -59,7 +59,7 @@ export async function validateAuthoringChanges(configFile, options) {
59
59
  // Internal injection boundary for deterministic capture/check race fixtures.
60
60
  export async function validateAuthoringChangesWithHooks(configFile, options, hooks = {}) {
61
61
  checkedOptions(options);
62
- const capture = await captureSnapshotInputs(configFile);
62
+ const capture = await captureSnapshotInputs(configFile, { profile: options.profile });
63
63
  const runtime = await validatorInputs();
64
64
  let baseline = { kind: 'explicit-declaration' };
65
65
  const previous = new Map();
@@ -67,20 +67,41 @@ export async function validateAuthoringChangesWithHooks(configFile, options, hoo
67
67
  const raw = await readFile(options.baselineFile);
68
68
  const value = parseAuthoringBaseline(JSON.parse(raw.toString('utf8')), capture.root, capture.config);
69
69
  baseline = { kind: 'task-baseline', file: await realpath(options.baselineFile), digest: sha256(raw) };
70
+ if (value.profile?.file !== capture.profile?.file)
71
+ throw new SnapshotError('invalid_input', 'Baseline and validation must use the same deployment profile path');
72
+ // Retain previously declared profile assets just like other baseline
73
+ // dependencies, even when a profile no longer references an extant file.
74
+ for (const [file, source] of Object.entries(value.profile?.sources ?? {})) {
75
+ if (!capture.profile || Object.hasOwn(capture.profile.sources, file))
76
+ continue;
77
+ const input = await readProfileInput(source).catch(error => { if (error.code === 'ENOENT')
78
+ return undefined; throw error; });
79
+ if (!input)
80
+ continue;
81
+ capture.profile.sources[file] = source;
82
+ capture.inputs.push({ file, digest: input.digest, mode: input.mode });
83
+ capture.buffers.set(file, input.bytes);
84
+ }
85
+ capture.inputs.sort((a, b) => a.file.localeCompare(b.file));
70
86
  for (const input of value.inputs)
71
87
  previous.set(input.file, input);
72
88
  // A dependency removed from config is not necessarily a deleted file. Retain
73
89
  // baseline scope for extant sibling inputs so change classification is honest.
74
90
  const oldExternal = [];
75
91
  for (const input of value.inputs)
76
- if ((!isWithin(capture.root, path.resolve(capture.root, input.file)) || input.file.split(path.sep).includes('node_modules')) && await stat(relativeInput(capture.root, input.file)).then(() => true, () => false))
92
+ if (!Object.hasOwn(capture.profile?.sources ?? {}, input.file) && (!isWithin(capture.root, path.resolve(capture.root, input.file)) || input.file.split(path.sep).includes('node_modules')) && await stat(relativeInput(capture.root, input.file)).then(() => true, () => false))
77
93
  oldExternal.push(input.file);
78
94
  const oldDirectories = [];
79
95
  for (const directory of value.externalDirectories)
80
96
  if (await stat(relativeInput(capture.root, directory)).then(() => true, () => false))
81
97
  oldDirectories.push(directory);
82
98
  const combinedDirectories = [...new Set([...capture.externalDirectories, ...oldDirectories])].sort();
83
- const combined = await collectSnapshotInputs(capture.root, [...new Set([...oldExternal, ...capture.inputs.filter(input => (!isWithin(capture.root, path.resolve(capture.root, input.file)) || input.file.split(path.sep).includes('node_modules'))).map(input => input.file)])], combinedDirectories);
99
+ const combined = await collectSnapshotInputs(capture.root, [...new Set([...oldExternal, ...capture.inputs.filter(input => !Object.hasOwn(capture.profile?.sources ?? {}, input.file) && (!isWithin(capture.root, path.resolve(capture.root, input.file)) || input.file.split(path.sep).includes('node_modules'))).map(input => input.file)])], combinedDirectories);
100
+ for (const input of capture.inputs.filter(input => Object.hasOwn(capture.profile?.sources ?? {}, input.file))) {
101
+ combined.inputs.push(input);
102
+ combined.buffers.set(input.file, capture.buffers.get(input.file));
103
+ }
104
+ combined.inputs.sort((a, b) => a.file.localeCompare(b.file));
84
105
  for (const input of capture.inputs)
85
106
  if (combined.inputs.find(current => current.file === input.file)?.digest !== input.digest)
86
107
  throw new SnapshotError('stale_input', `Input changed while comparing the task baseline: ${input.file}`);
@@ -90,7 +111,7 @@ export async function validateAuthoringChangesWithHooks(configFile, options, hoo
90
111
  // Loading configuration is read-only. Compilation and executable extensions
91
112
  // are never run on the original project.
92
113
  try {
93
- loaded = await loadConfig(path.join(capture.root, capture.config));
114
+ loaded = await loadConfig(path.join(capture.root, capture.config), { profile: options.profile });
94
115
  }
95
116
  catch { /* Snapshot compiler diagnoses invalid YAML/config. */ }
96
117
  const contentDir = path.resolve(capture.root, String(loaded?.value.site?.contentDir ?? 'content'));
@@ -147,7 +168,7 @@ export async function validateAuthoringChangesWithHooks(configFile, options, hoo
147
168
  await mkdir(directory);
148
169
  }
149
170
  const tree = path.join(directory, 'tree'), shadow = path.join(tree, path.basename(capture.root)), site = path.join(directory, 'site');
150
- const report = { schemaVersion: 1, kind: 'shoalrun-authoring-validation', status: 'incomplete', snapshotId, project: capture.root, config: capture.config, baseline, changed, inputs: capture.inputs, excluded: capture.excluded, externalDirectories: capture.externalDirectories, contentDigest, configDigest: capture.configDigest, validatorDigest: runtime.digest, runtimeDependencies: [], dependencies: [], checks: [], findings: [],
171
+ const report = { profile: capture.profile, schemaVersion: 1, kind: 'shoalrun-authoring-validation', status: 'incomplete', snapshotId, project: capture.root, config: capture.config, baseline, changed, inputs: capture.inputs, excluded: capture.excluded, externalDirectories: capture.externalDirectories, contentDigest, configDigest: capture.configDigest, validatorDigest: runtime.digest, runtimeDependencies: [], dependencies: [], checks: [], findings: [],
151
172
  coverage: { scope: 'complete-captured-project', authored, affected, rendered: [], notRendered: [...affected] },
152
173
  artifacts: { directory, project: shadow, site, report: path.join(directory, 'report.json'), digest: null, lifecycle: 'Retained local validation artifacts. Remove this artifact directory when finished; it is separate from working-tree source. No preview service is started.' },
153
174
  freshness: { current: true, checkedAt: new Date().toISOString() } };
@@ -175,7 +196,8 @@ export async function validateAuthoringChangesWithHooks(configFile, options, hoo
175
196
  if (!await confirmSnapshotInputs(capture) || !await absentInputsUnchanged())
176
197
  throw new SnapshotError('stale_input', 'Working tree changed during snapshot capture; retry validation.');
177
198
  const stagedConfig = path.join(shadow, capture.config);
178
- loaded = await loadConfig(stagedConfig);
199
+ const stagedProfile = capture.profile ? path.resolve(shadow, capture.profile.file) : undefined;
200
+ loaded = await loadConfig(stagedConfig, { profile: stagedProfile });
179
201
  if (capture.configDigest && loaded.digest !== capture.configDigest)
180
202
  throw new SnapshotError('stale_input', 'Captured configuration does not match the inspected configuration; retry.');
181
203
  // Local executable metadata/highlighting modules are not a provider-free
@@ -198,7 +220,7 @@ export async function validateAuthoringChangesWithHooks(configFile, options, hoo
198
220
  }
199
221
  }
200
222
  try {
201
- const manifest = await compile(stagedConfig, site, {}, { authoringFiles: new Set(affected.map(file => path.join(shadow, file))) });
223
+ const manifest = await compile(stagedConfig, site, {}, { profile: stagedProfile, authoringFiles: new Set(affected.map(file => path.join(shadow, file))) });
202
224
  report.dependencies = manifest.dependencies.map(file => isWithin(tree, file) ? path.relative(shadow, file) : file);
203
225
  for (const dependency of manifest.dependencies) {
204
226
  if (isWithin(tree, dependency)) {
@@ -212,8 +234,12 @@ export async function validateAuthoringChangesWithHooks(configFile, options, hoo
212
234
  }
213
235
  report.coverage.rendered = (manifest.authoringSources ?? []).map(doc => ({ id: doc.id, file: path.relative(shadow, doc.file), route: doc.url, markdown: doc.markdownUrl }));
214
236
  report.coverage.notRendered = affected.filter(file => !report.coverage.rendered.some(doc => doc.file === file));
215
- for (const file of report.coverage.notRendered)
216
- report.findings.push({ ...finding('validation.excluded-page', 'This affected authored page did not render in any enabled publication.', 'Correct its publication/locale selection or enable an isolated review publication and validate again.'), file });
237
+ for (const file of report.coverage.notRendered) {
238
+ const profileExcluded = manifest.profileSelection?.decisions.some(decision => !decision.included && decision.id.startsWith(path.join(shadow, file) + ': '));
239
+ report.findings.push(profileExcluded
240
+ ? { ...finding('validation.profile-excluded', 'This affected page is excluded from the selected deployment edition; its rendered content was not checked.', 'Validate a profile/publication that includes this page before accepting changes to its content.', 'warning'), file }
241
+ : { ...finding('validation.excluded-page', 'This affected authored page did not render in any enabled publication.', 'Correct its publication/locale selection or enable an isolated review publication and validate again.'), file });
242
+ }
217
243
  report.checks.push({ id: 'compile', status: 'passed', documents: manifest.documents.length });
218
244
  const settings = parseAuditSettings(config.audit);
219
245
  if (settings.links) {
@@ -324,6 +350,7 @@ export async function readValidationReport(reportFile) {
324
350
  throw new SnapshotError('invalid_input', 'Expected a version-1 authoring validation report');
325
351
  if (!report.baseline || !report.coverage || !Array.isArray(report.coverage.rendered) || !Array.isArray(report.coverage.affected) || !Array.isArray(report.coverage.notRendered) || !Array.isArray(report.checks) || !Array.isArray(report.findings) || !report.artifacts || !report.freshness || typeof report.project !== 'string' || !path.isAbsolute(report.project) || typeof report.snapshotId !== 'string' || !/^[a-f0-9]{64}$/.test(report.snapshotId))
326
352
  throw new SnapshotError('invalid_input', 'Malformed authoring validation report');
353
+ validateProfileCapture(report.project, report.profile);
327
354
  for (const input of report.inputs)
328
355
  relativeInput(report.project, input.file);
329
356
  return report;
@@ -335,7 +362,7 @@ export async function authoringValidationStatus(reportFile) {
335
362
  for (const input of report.changed)
336
363
  if (input.digest === null && await stat(relativeInput(report.project, input.file)).then(() => true, () => false))
337
364
  missingInputCreated = true;
338
- const changed = missingInputCreated || sha256(JSON.stringify(report.inputs)) !== report.contentDigest || !await confirmSnapshotInputs({ root: report.project, inputs: report.inputs, externalDirectories: report.externalDirectories });
365
+ const changed = missingInputCreated || sha256(JSON.stringify(report.inputs)) !== report.contentDigest || !await confirmSnapshotInputs({ root: report.project, inputs: report.inputs, externalDirectories: report.externalDirectories, profile: report.profile });
339
366
  const runtime = await validatorInputs();
340
367
  let baselineChanged = false, dependencyChanged = false;
341
368
  if (report.baseline.file)
@@ -1,4 +1,4 @@
1
- export declare const docsHelp = "Usage: shoalrun docs <command>\n\nRead bundled references (no project or provider key required):\n search <query> [--json]\n read <document-id> [--json]\n\nInspect a project:\n project [--config FILE] [--section pages|guidance|configuration|navigation|relationships|templates|findings]\n source FILE [--config FILE]\n context <document-id> [--config FILE]\n\nValidate and preview edits:\n baseline --out FILE [--config FILE]\n validate (--baseline FILE | --file FILE | --id ID) [--config FILE]\n validation-status REPORT\n review REPORT [--format json|markdown]\n preview REPORT [--port PORT]\n\nOther authoring tools: changes, plan, apply, mcp.\nDiscover packaged agent guidance with shoalrun skills list.\n";
1
+ export declare const docsHelp = "Usage: shoalrun docs <command>\n\nRead bundled references (no project or provider key required):\n search <query> [--json]\n read <document-id> [--json]\n\nInspect a project:\n project [--config FILE] [--profile FILE] [--section pages|guidance|configuration|navigation|relationships|templates|findings]\n source FILE [--config FILE] [--profile FILE]\n context <document-id> [--config FILE]\n\nValidate and preview edits:\n baseline --out FILE [--config FILE] [--profile FILE]\n validate (--baseline FILE | --file FILE | --id ID) [--config FILE] [--profile FILE]\n validation-status REPORT\n review REPORT [--format json|markdown]\n preview REPORT [--port PORT]\n\nOther authoring tools: changes, plan, apply, mcp.\nDiscover packaged agent guidance with shoalrun skills list.\n";
2
2
  export declare const skillsHelp = "Usage: shoalrun skills list [--json]\n shoalrun skills read <name> [--json]\n\nList the names, descriptions and local paths of packaged skills, or read a skill.\nThese commands do not install skills or require a project or provider key.\n";
3
- export declare const cliHelp = "Usage: shoalrun <command>\n\nStart and build:\n init [directory] [--starter minimal|api-docs|showcase]\n build [config] [output]\n export [config] --out DIRECTORY [--target NAME]\n dev [config] [output] [--port PORT]\n serve <site-directory> [--host HOST] [--port PORT]\n check [config] [--json]\n audit [config]\n scaffold <template> -n <target> [--config FILE]\n\nAgent guidance and offline documentation:\n skills list [--json]\n skills read <name> [--json]\n docs search <query> [--json]\n docs read <document-id> [--json]\n docs project [--config FILE]\n docs baseline --out FILE [--config FILE]\n docs validate (--baseline FILE | --file FILE | --id ID) [--config FILE]\n docs review REPORT [--format json|markdown]\n docs preview REPORT [--port PORT]\n\nOther commands: config, api, operation, ingredient, recipe, search, index,\nask, answers, ask-log, chat, eval, vercel.\nRun shoalrun docs --help or shoalrun skills --help for discovery commands.\n";
3
+ export declare const cliHelp = "Usage: shoalrun <command>\n\nStart and build:\n init [directory] [--starter minimal|api-docs|showcase]\n build [config] [output] [--profile FILE]\n export [config] --out DIRECTORY [--target NAME] [--profile FILE]\n dev [config] [output] [--port PORT]\n serve <site-directory> [--host HOST] [--port PORT]\n check [config] [--json]\n audit [config]\n scaffold <template> -n <target> [--config FILE]\n\nAgent guidance and offline documentation:\n skills list [--json]\n skills read <name> [--json]\n docs search <query> [--json]\n docs read <document-id> [--json]\n docs project [--config FILE] [--profile FILE]\n docs baseline --out FILE [--config FILE] [--profile FILE]\n docs validate (--baseline FILE | --file FILE | --id ID) [--config FILE] [--profile FILE]\n docs review REPORT [--format json|markdown]\n docs preview REPORT [--port PORT]\n\nOther commands: config, api, operation, ingredient, recipe, search, index,\nask, answers, ask-log, chat, eval, vercel.\nRun shoalrun docs --help or shoalrun skills --help for discovery commands.\n";
4
4
  export declare function runSkillsCommand(args: string[]): Promise<string>;
@@ -9,13 +9,13 @@ Read bundled references (no project or provider key required):
9
9
  read <document-id> [--json]
10
10
 
11
11
  Inspect a project:
12
- project [--config FILE] [--section pages|guidance|configuration|navigation|relationships|templates|findings]
13
- source FILE [--config FILE]
12
+ project [--config FILE] [--profile FILE] [--section pages|guidance|configuration|navigation|relationships|templates|findings]
13
+ source FILE [--config FILE] [--profile FILE]
14
14
  context <document-id> [--config FILE]
15
15
 
16
16
  Validate and preview edits:
17
- baseline --out FILE [--config FILE]
18
- validate (--baseline FILE | --file FILE | --id ID) [--config FILE]
17
+ baseline --out FILE [--config FILE] [--profile FILE]
18
+ validate (--baseline FILE | --file FILE | --id ID) [--config FILE] [--profile FILE]
19
19
  validation-status REPORT
20
20
  review REPORT [--format json|markdown]
21
21
  preview REPORT [--port PORT]
@@ -33,8 +33,8 @@ export const cliHelp = `Usage: shoalrun <command>
33
33
 
34
34
  Start and build:
35
35
  init [directory] [--starter minimal|api-docs|showcase]
36
- build [config] [output]
37
- export [config] --out DIRECTORY [--target NAME]
36
+ build [config] [output] [--profile FILE]
37
+ export [config] --out DIRECTORY [--target NAME] [--profile FILE]
38
38
  dev [config] [output] [--port PORT]
39
39
  serve <site-directory> [--host HOST] [--port PORT]
40
40
  check [config] [--json]
@@ -46,9 +46,9 @@ Agent guidance and offline documentation:
46
46
  skills read <name> [--json]
47
47
  docs search <query> [--json]
48
48
  docs read <document-id> [--json]
49
- docs project [--config FILE]
50
- docs baseline --out FILE [--config FILE]
51
- docs validate (--baseline FILE | --file FILE | --id ID) [--config FILE]
49
+ docs project [--config FILE] [--profile FILE]
50
+ docs baseline --out FILE [--config FILE] [--profile FILE]
51
+ docs validate (--baseline FILE | --file FILE | --id ID) [--config FILE] [--profile FILE]
52
52
  docs review REPORT [--format json|markdown]
53
53
  docs preview REPORT [--port PORT]
54
54
 
package/dist/cli.js CHANGED
@@ -73,7 +73,7 @@ function buildArguments(args, defaultOutput) {
73
73
  positional.push(value);
74
74
  continue;
75
75
  }
76
- if (!['--source-commit', '--release-label'].includes(value))
76
+ if (!['--source-commit', '--release-label', '--profile'].includes(value))
77
77
  throw new Error(`Unknown build option ${value}`);
78
78
  if (flags[value] !== undefined)
79
79
  throw new Error(`Duplicate build option ${value}`);
@@ -84,7 +84,7 @@ function buildArguments(args, defaultOutput) {
84
84
  }
85
85
  if (positional.length > 2)
86
86
  throw new Error('Build accepts at most a config and output directory');
87
- return { config: positional[0] ?? 'docs.config.yaml', output: positional[1] ?? defaultOutput, provenance: { sourceCommit: flags['--source-commit'], releaseLabel: flags['--release-label'] } };
87
+ return { profile: flags['--profile'], config: positional[0] ?? 'docs.config.yaml', output: positional[1] ?? defaultOutput, provenance: { sourceCommit: flags['--source-commit'], releaseLabel: flags['--release-label'] } };
88
88
  }
89
89
  // An unset or empty PORT means the default; serveSite rejects anything else that is not a port number.
90
90
  const defaultPort = () => process.env.PORT?.trim() ? Number(process.env.PORT) : 8765;
@@ -134,8 +134,8 @@ try {
134
134
  }
135
135
  }
136
136
  else if (command === 'build') {
137
- const { config, output, provenance } = buildArguments([subcommand, ...rest].filter((item) => item !== undefined), 'site-dist');
138
- const manifest = await compile(config, output, provenance);
137
+ const { config, output, provenance, profile } = buildArguments([subcommand, ...rest].filter((item) => item !== undefined), 'site-dist');
138
+ const manifest = await compile(config, output, provenance, { profile });
139
139
  process.stdout.write(JSON.stringify({ status: 'ok', documents: manifest.documents.length, operations: Object.keys(manifest.operations).length }, null, 2) + '\n');
140
140
  }
141
141
  else if (command === 'dev') {
@@ -160,8 +160,10 @@ try {
160
160
  else if (command === 'vercel' && subcommand === 'build') {
161
161
  // --chat adds the optional answer function: it needs chat and retrieval settings and the embedding key at build time.
162
162
  const chat = rest.includes('--chat');
163
- const { config, output, provenance } = buildArguments(rest.filter(item => item !== '--chat'), '.vercel/output');
164
- const result = await buildVercel(config, output, provenance, { chat });
163
+ const { config, output, provenance, profile } = buildArguments(rest.filter(item => item !== '--chat'), '.vercel/output');
164
+ if (profile && chat)
165
+ throw new Error('Profiled Vercel builds do not support --chat yet; build the selected static edition without --chat');
166
+ const result = await buildVercel(config, output, provenance, { chat, profile });
165
167
  // Asking for the chat and not getting it must fail the deploy. Otherwise a site would look ready and have no chat.
166
168
  if (chat && result.chat && !result.chat.added)
167
169
  throw new Error(`The chat was requested but not added: ${result.chat.reason}`);
@@ -10,6 +10,7 @@ export declare function compile(configFile: string, destination?: string, proven
10
10
  apiReview?: 'report';
11
11
  sourceOverrides?: ReadonlyMap<string, string>;
12
12
  authoringFiles?: ReadonlySet<string>;
13
+ profile?: string;
13
14
  }): Promise<{
14
15
  authoringSources?: {
15
16
  id: string;
@@ -36,6 +37,18 @@ export declare function compile(configFile: string, destination?: string, proven
36
37
  }>;
37
38
  }>;
38
39
  }[];
40
+ profileSelection: {
41
+ schemaVersion: number;
42
+ profileDigest: string;
43
+ features: string[];
44
+ provenance: Record<string, string[]>;
45
+ decisions: {
46
+ kind: "page" | "section";
47
+ id: string;
48
+ included: boolean;
49
+ requires?: import("./deployment-profile.js").FeatureRequirements;
50
+ }[];
51
+ } | undefined;
39
52
  documents: ({
40
53
  digest: string;
41
54
  markdownBytes: number;
package/dist/compiler.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { validateHeader, validateFooter } from './site-identity.js';
2
+ import { profileSelection } from './deployment-profile.js';
3
+ import { profileNavigation } from './project-navigation.js';
1
4
  import { canonical, checkOutputDirectory } from './output-location.js';
2
5
  import { reviewApiPublication } from './api-publication-review.js';
3
6
  import { prepareContentWorkspace } from './content-workspace.js';
@@ -74,75 +77,6 @@ function validateSearch(value, declaringFile) {
74
77
  const builtInSearchProvider = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../assets/search-lexical.js');
75
78
  // Each shape preset has a default palette; palettes live in styles/themes/palettes.
76
79
  export const themePresets = Object.fromEntries(presetCatalog.map(preset => [preset.name, preset.palette]));
77
- function validateHeader(value) {
78
- if (value === undefined)
79
- return { links: [] };
80
- if (!value || typeof value !== 'object' || Array.isArray(value))
81
- throw new Error('site.header must be a mapping');
82
- const { links = [], ...extra } = value;
83
- if (Object.keys(extra).length)
84
- throw new Error(`site.header does not accept ${Object.keys(extra).join(', ')}`);
85
- if (!Array.isArray(links))
86
- throw new Error('site.header.links must be a list');
87
- return {
88
- links: links.map((link, index) => {
89
- const where = `site.header.links[${index}]`;
90
- const { style = 'link', ...rest } = (link && typeof link === 'object' && !Array.isArray(link) ? link : {});
91
- const checked = validateSiteLink(link, where, rest);
92
- if (style !== 'link' && style !== 'button' && style !== 'primary')
93
- throw new Error(`${where}.style must be link, button, or primary`);
94
- return { ...checked, style };
95
- })
96
- };
97
- }
98
- // Header and footer links share one rule: a label and a site path, http(s) URL, or mailto link.
99
- function validateSiteLink(link, where, fields = link) {
100
- if (!link || typeof link !== 'object' || Array.isArray(link))
101
- throw new Error(`${where} must be a mapping`);
102
- const { label, href, ...rest } = fields;
103
- if (Object.keys(rest).length)
104
- throw new Error(`${where} does not accept ${Object.keys(rest).join(', ')}`);
105
- if (typeof label !== 'string' || !label.trim())
106
- throw new Error(`${where}.label is required`);
107
- if (typeof href !== 'string' || !/^(\/|https?:\/\/|mailto:)/.test(href))
108
- throw new Error(`${where}.href must be a site path, http(s) URL, or mailto link`);
109
- return { label, href };
110
- }
111
- function validateFooter(value) {
112
- if (value === undefined)
113
- return undefined;
114
- if (!value || typeof value !== 'object' || Array.isArray(value))
115
- throw new Error('site.footer must be a mapping');
116
- const { columns = [], links = [], text, ...extra } = value;
117
- if (Object.keys(extra).length)
118
- throw new Error(`site.footer does not accept ${Object.keys(extra).join(', ')}`);
119
- if (!Array.isArray(columns))
120
- throw new Error('site.footer.columns must be a list');
121
- if (!Array.isArray(links))
122
- throw new Error('site.footer.links must be a list');
123
- if (text !== undefined && (typeof text !== 'string' || !text.trim()))
124
- throw new Error('site.footer.text must be non-empty text');
125
- const footer = {
126
- columns: columns.map((column, index) => {
127
- const where = `site.footer.columns[${index}]`;
128
- if (!column || typeof column !== 'object' || Array.isArray(column))
129
- throw new Error(`${where} must be a mapping`);
130
- const { title, links: columnLinks, ...rest } = column;
131
- if (Object.keys(rest).length)
132
- throw new Error(`${where} does not accept ${Object.keys(rest).join(', ')}`);
133
- if (typeof title !== 'string' || !title.trim())
134
- throw new Error(`${where}.title is required`);
135
- if (!Array.isArray(columnLinks) || !columnLinks.length)
136
- throw new Error(`${where}.links must list at least one link`);
137
- return { title, links: columnLinks.map((link, linkIndex) => validateSiteLink(link, `${where}.links[${linkIndex}]`)) };
138
- }),
139
- links: links.map((link, index) => validateSiteLink(link, `site.footer.links[${index}]`)),
140
- ...(typeof text === 'string' ? { text } : {})
141
- };
142
- if (!footer.columns.length && !footer.links.length && !footer.text)
143
- throw new Error('site.footer needs columns, links, or text');
144
- return footer;
145
- }
146
80
  const contrastFailuresFor = (palette, modes, calloutTypes, badgeVariants) => {
147
81
  const customTones = new Map([...calloutTypes.values()].flatMap(type => type.colors ? [[type.tone, type.colors]] : []));
148
82
  return [...contrastProblems(palette, modes), ...calloutToneProblems(customTones, palette, modes), ...badgeContrastProblems(badgeVariants, palette, modes)];
@@ -165,7 +99,7 @@ export async function checkThemeContrast(configFile) {
165
99
  return { modes, palette: palette.name };
166
100
  }
167
101
  export async function compile(configFile, destination, provenance = {}, options = {}) {
168
- const loaded = await loadConfig(configFile);
102
+ const loaded = await loadConfig(configFile, { profile: options.profile });
169
103
  const componentPlugins = await loadComponentPlugins(loaded);
170
104
  const provenanceRecord = buildInfo(loaded.digest, provenance);
171
105
  const resolvedConfig = resolvedCompilerConfig(loaded.value);
@@ -246,6 +180,10 @@ export async function compile(configFile, destination, provenance = {}, options
246
180
  if (faviconFile && !existsSync(faviconFile))
247
181
  throw new Error(`site.favicon: file not found: ${faviconFile}`);
248
182
  const faviconRoute = publicPath(faviconFile?.toLowerCase().endsWith('.png') ? '/favicon.png' : '/favicon.svg');
183
+ if (config.site.logo !== undefined && (typeof config.site.logo !== 'string' || !/\.(?:svg|png)$/i.test(config.site.logo)))
184
+ throw new Error('site.logo must be a local .svg or .png file');
185
+ const logoFile = config.site.logo === undefined ? undefined : path.resolve(path.dirname(explainSegments(loaded, ['site', 'logo']).source.file), config.site.logo);
186
+ const logoRoute = logoFile ? publicPath(`/assets/brand-logo${path.extname(logoFile).toLowerCase()}`) : undefined;
249
187
  const shareFontFiles = [...bundledShareFonts(), ...shareImages.fonts, ...fonts.faces.map(face => face.file).filter(file => /\.(?:ttf|otf)$/i.test(file))];
250
188
  const shareCache = path.join(base, '.shoalrun/cache/share-images');
251
189
  const fontDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../styles/fonts');
@@ -353,7 +291,7 @@ export async function compile(configFile, destination, provenance = {}, options
353
291
  throw new Error('site.contentFilter matched no published pages; check page facets and enabled publications');
354
292
  let navigationSpec;
355
293
  try {
356
- navigationSpec = validateProjectNavigation(config.navigation ?? [], docs, knownOperations, new Set(Object.keys(config.apis ?? {})));
294
+ navigationSpec = validateProjectNavigation(loaded.deployment?.context.grants && loaded.deployment.context.pruneNavigation ? profileNavigation(config.navigation ?? [], docs, knownDocumentIds, knownOperations, new Set(Object.keys(config.apis ?? {}))) : config.navigation ?? [], docs, knownOperations, new Set(Object.keys(config.apis ?? {})));
357
295
  }
358
296
  catch (error) {
359
297
  const source = config.navigation === undefined ? configFile : explain(loaded, 'navigation').source.file;
@@ -390,10 +328,20 @@ export async function compile(configFile, destination, provenance = {}, options
390
328
  };
391
329
  // Local component and Markdown images publish beside their page under /assets/pages.
392
330
  const heroImages = new Map();
331
+ if (logoFile && logoRoute)
332
+ heroImages.set(logoRoute, logoFile);
393
333
  const walkthroughSources = new Set();
334
+ const usedComponentAssets = new Set();
394
335
  const collectPageAssets = (value) => {
395
336
  if (!isConfigMapping(value))
396
337
  return;
338
+ if (value.type === 'docComponent' && isConfigMapping(value.browser)) {
339
+ if (typeof value.browser.moduleUrl === 'string')
340
+ usedComponentAssets.add(value.browser.moduleUrl);
341
+ for (const route of Array.isArray(value.browser.styleUrls) ? value.browser.styleUrls : [])
342
+ if (typeof route === 'string')
343
+ usedComponentAssets.add(route);
344
+ }
397
345
  const image = value.image;
398
346
  if (value.type === 'docHero' && isConfigMapping(image) && typeof image.url === 'string' && typeof image.file === 'string') {
399
347
  const route = publicPath(image.url);
@@ -428,7 +376,7 @@ export async function compile(configFile, destination, provenance = {}, options
428
376
  rendered, docs, relationships, knownOperations, cardUses, publishedRelationships, cookbooks, documentRoutes,
429
377
  basePath, siteOrigin, colorMode, config, highlighter, editionLabel, customStyleFile, themePreset, palette,
430
378
  themeStyleFile, generatedPaletteCss, generatedBadgeCss, themeSwitcherData, generatedFontsCss, fontsPreload: fonts.preload,
431
- shareFor, faviconRoute, modeToggle, header, footer, search, readerFilters, variants, externalDocuments,
379
+ shareFor, faviconRoute, logoRoute, modeToggle, header, footer, search, readerFilters, variants, externalDocuments,
432
380
  externalSiteLinks, navigationSpec, navigationEntries, navigationTagOrder
433
381
  });
434
382
  // The theme previewer and switcher re-theme pages in the browser, so every preset and palette ships only when one is used.
@@ -501,11 +449,12 @@ export async function compile(configFile, destination, provenance = {}, options
501
449
  const artifactConfig = options.authoringFiles ? { ...config, mcp: false } : config;
502
450
  const { artifacts, buildId } = await assembleArtifacts({
503
451
  base, basePath, publicPath, canonical, siteOrigin, manifest, provenanceRecord, rendered, sitemaps,
452
+ privateDependencies: new Set([...(loaded.deployment?.file ? [loaded.deployment.file] : []), ...(loaded.deployment?.assetFiles ?? [])]),
504
453
  diagramAssets, heroImages, focusedContracts, styleFiles, themeStyleFile, generatedPaletteCss,
505
454
  generatedBadgeCss, search, synonyms, config: artifactConfig, builtInSearchProvider, customReaderModule, analyticsModule, mcpAnalyticsModule,
506
455
  previewFiles, assetDirectory, themeSwitcher, themeDirectory, palette, colorMode, generatedFontsCss,
507
456
  fontFiles, fonts, shareImages, shareFor, themePreset, shareFontFiles, shareCache, faviconFile,
508
- faviconRoute, customStyleFile, scriptFiles, componentAssets: componentPlugins.assets, redirects, apiPublications, api, catalog, headers,
457
+ faviconRoute, customStyleFile, scriptFiles, componentAssets: loaded.deployment?.context.grants ? componentPlugins.assets.filter(asset => usedComponentAssets.has(asset.route)) : componentPlugins.assets, redirects, apiPublications, api, catalog, headers,
509
458
  documentRoutes, readerFilters, apiSources
510
459
  });
511
460
  if (destination) {
@@ -516,10 +465,12 @@ export async function compile(configFile, destination, provenance = {}, options
516
465
  await mkdir(localGuide, { recursive: true });
517
466
  await writeFile(path.join(localGuide, 'frontmatter-reference.md'), frontmatterReference(config.frontmatter));
518
467
  await writeFile(path.join(localGuide, 'frontmatter.schema.json'), JSON.stringify(frontmatterSchema(config.frontmatter), null, 2) + '\n');
468
+ if (loaded.deployment?.context.grants && !options.authoringFiles)
469
+ await writeFile(path.join(localGuide, 'profile-selection.json'), JSON.stringify({ ...profileSelection(loaded.deployment), buildId, configDigest: loaded.digest }, null, 2) + '\n');
519
470
  });
520
471
  // Authoring captures resolve revision dates without changing captured inputs.
521
472
  if (!options.authoringFiles)
522
473
  await freshness?.commit();
523
474
  }
524
- return { ...manifest, buildId, apiPublicationReview, ...(options.authoringFiles ? { authoringSources: rendered.map(({ doc }) => ({ id: doc.frontmatter.id, file: doc.file, url: doc.route, markdownUrl: doc.markdownRoute })) } : {}) };
475
+ return { ...manifest, buildId, apiPublicationReview, profileSelection: profileSelection(loaded.deployment), ...(options.authoringFiles ? { authoringSources: rendered.map(({ doc }) => ({ id: doc.frontmatter.id, file: doc.file, url: doc.route, markdownUrl: doc.markdownRoute })) } : {}) };
525
476
  }
@@ -1 +1 @@
1
- export const builtinComponentNames = new Set('Anchor FrontmatterReference ForPublication ReaderSection ReaderChoice ReaderFilter Only Badge Kbd Tooltip InfoHint Progress Math ActionLink Button DefinitionList Term Definition Steps Timeline Milestone FileTree Tree Folder File Table CollapsibleGroup Collapsible Section Hero ThemePreview Diagram Image Tabs Walkthrough Step Tab Fields Field CookbookChooser DocCard DocCards DocCollection RelatedReading ApiExample ApiSchema Snippet Var DocLink Callout'.split(' '));
1
+ export const builtinComponentNames = new Set('Anchor FrontmatterReference ForFeature ForPublication ReaderSection ReaderChoice ReaderFilter Only Badge Kbd Tooltip InfoHint Progress Math ActionLink Button DefinitionList Term Definition Steps Timeline Milestone FileTree Tree Folder File Table CollapsibleGroup Collapsible Section Hero ThemePreview Diagram Image Tabs Walkthrough Step Tab Fields Field CookbookChooser DocCard DocCards DocCollection RelatedReading ApiExample ApiSchema Snippet Var DocLink Callout'.split(' '));
@@ -1,3 +1,4 @@
1
+ import { pruneFeatureTree } from './deployment-profile.js';
1
2
  import { lowerComponent } from './component-plugins.js';
2
3
  import { cardComponent } from './cards.js';
3
4
  import { cookbookChooserComponent } from './cookbook-chooser.js';
@@ -59,7 +60,8 @@ function literalAttributes(attributes) {
59
60
  export function transformMdx(tree, current, registry, snippets = new Map(), projectValues = {}, uses = [], calloutTypes = resolveCalloutTypes(undefined, current.file), projectRoot, readerConfig, frontmatterConfig = {}, publicationConfig, badgeVariants = resolveBadgeVariants(undefined, current.file), componentPlugins = new Map()) {
60
61
  if (!isMdxRoot(tree))
61
62
  throw new Error(`${current.file}: parsed content must be a root with children`);
62
- tree = lowerSectionMarkers(tree, current.file);
63
+ tree = lowerSectionMarkers(pruneFeatureTree(tree, current.featureContext, current.file), current.file);
64
+ snippets = new Map([...snippets].map(([id, definition]) => [id, { ...definition, tree: pruneFeatureTree(definition.tree, current.featureContext, definition.file) }]));
63
65
  let pluginDepth = 0;
64
66
  let tabsIndex = 0;
65
67
  let walkthroughIndex = 0;
@@ -130,6 +132,10 @@ export function transformMdx(tree, current, registry, snippets = new Map(), proj
130
132
  throw new Error(`${sourceFile}: <FrontmatterReference> must be an empty block`);
131
133
  return childrenOf(parsePlainMarkdown(frontmatterReference(frontmatterConfig, true)), scope, stack, sourceFile);
132
134
  }
135
+ if (node.name === 'ForFeature') {
136
+ const selected = pruneFeatureTree({ type: 'root', children: [node] }, current.featureContext, sourceFile);
137
+ return childrenOf(selected.children, scope, stack, sourceFile);
138
+ }
133
139
  if (node.name === 'ForPublication') {
134
140
  const attributes = node.attributes ?? [];
135
141
  const allowed = ['channel', 'version', 'language'];
package/dist/config.d.ts CHANGED
@@ -8,10 +8,13 @@ type Tree = {
8
8
  };
9
9
  export declare class ConfigError extends Error {
10
10
  }
11
- export declare function loadConfig(file: string): Promise<{
11
+ export declare function loadConfig(file: string, options?: {
12
+ profile?: string;
13
+ }): Promise<{
12
14
  value: Record<string, unknown>;
13
15
  tree: Tree;
14
16
  dependencies: string[];
17
+ deployment: import("./deployment-profile.js").ProfileResolution | undefined;
15
18
  digest: string;
16
19
  }>;
17
20
  export declare function explain(config: Awaited<ReturnType<typeof loadConfig>>, key: string): {
package/dist/config.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { resolveDeploymentProfile } from './deployment-profile.js';
1
2
  import { validateExports } from './content-export-config.js';
2
3
  import { validateContentFilter } from './content-filter.js';
3
4
  import { validateReaderFilters } from './reader-filters.js';
@@ -8,7 +9,7 @@ import { parseYaml } from './yaml-value.js';
8
9
  import { validateSearch } from './search-config.js';
9
10
  import { resolveTaxonomy } from './taxonomy.js';
10
11
  import { validateCookbooks } from './cookbook-chooser.js';
11
- const allowed = new Set(['exports', 'schemaVersion', 'site', 'navigation', 'redirects', 'apis', 'relationships', 'variables', 'snippets', 'callouts', 'badges', 'publications', 'outputs', 'templates', 'theme', 'frontmatter', 'metadata', 'audit', 'readerFilters', 'retrieval', 'chat', 'authoring', 'search', 'cookbooks', 'analytics', 'mcp', 'components']);
12
+ const allowed = new Set(['profiles', 'exports', 'schemaVersion', 'site', 'navigation', 'redirects', 'apis', 'relationships', 'variables', 'snippets', 'callouts', 'badges', 'publications', 'outputs', 'templates', 'theme', 'frontmatter', 'metadata', 'audit', 'readerFilters', 'retrieval', 'chat', 'authoring', 'search', 'cookbooks', 'analytics', 'mcp', 'components']);
12
13
  const object = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
13
14
  const treeObject = (v) => object(v) && !('kind' in v && 'items' in v);
14
15
  export class ConfigError extends Error {
@@ -59,7 +60,7 @@ function merge(base, overlay) {
59
60
  return overlay;
60
61
  return { value: overlay.value, source: { file: overlay.source.file, history: [base.source.file, ...base.source.history] } };
61
62
  }
62
- export async function loadConfig(file) {
63
+ export async function loadConfig(file, options = {}) {
63
64
  const dependencies = new Set();
64
65
  const root = path.resolve(file);
65
66
  async function read(current, stack, rootFile = false) {
@@ -113,8 +114,21 @@ export async function loadConfig(file) {
113
114
  delete local.extends;
114
115
  return merge(result, await resolve(local));
115
116
  }
116
- const tree = finalize(await read(root, [], true));
117
- const value = valueOf(tree);
117
+ let tree = finalize(await read(root, [], true));
118
+ let value = valueOf(tree);
119
+ if (!object(value) || value.schemaVersion !== 1)
120
+ throw new ConfigError(`${root}: schemaVersion must be 1`);
121
+ const raw = value;
122
+ const profileSettings = treeObject(tree.value) ? tree.value.profiles : undefined;
123
+ const manifestNode = profileSettings && treeObject(profileSettings.value) ? profileSettings.value.manifest : undefined;
124
+ const deployment = await resolveDeploymentProfile(raw.profiles, raw, manifestNode?.source.file ?? root, options.profile);
125
+ if (deployment) {
126
+ for (const file of deployment.dependencies)
127
+ dependencies.add(file);
128
+ const node = (value) => ({ source: { file: deployment.file ?? root, history: [] }, value: Array.isArray(value) ? value.map(node) : object(value) ? Object.fromEntries(Object.entries(value).map(([key, child]) => [key, node(child)])) : value });
129
+ tree = finalize(merge(tree, node(deployment.overlay)));
130
+ value = valueOf(tree);
131
+ }
118
132
  if (!object(value) || value.schemaVersion !== 1)
119
133
  throw new ConfigError(`${root}: schemaVersion must be 1`);
120
134
  for (const key of Object.keys(value))
@@ -128,7 +142,7 @@ export async function loadConfig(file) {
128
142
  resolveTaxonomy(object(value.frontmatter) ? value.frontmatter.taxonomy : undefined);
129
143
  if (object(value.publications) && ((object(value.publications.defaults) && Object.hasOwn(value.publications.defaults, 'locale')) || (Array.isArray(value.publications.enabled) && value.publications.enabled.some(item => object(item) && Object.hasOwn(item, 'locale')))))
130
144
  throw new ConfigError(`${root}: publications.locale is now publications.language; page locale is reserved for formatting conventions`);
131
- return { value, tree, dependencies: [...dependencies].sort(), digest: createHash('sha256').update(JSON.stringify(value)).digest('hex') };
145
+ return { value, tree, dependencies: [...dependencies].sort(), deployment, digest: createHash('sha256').update(JSON.stringify(deployment ? [value, deployment.digest] : value)).digest('hex') };
132
146
  }
133
147
  export function explain(config, key) {
134
148
  return explainSegments(config, key.split('.'));
@@ -1,9 +1,21 @@
1
- export declare const exportHelp = "Usage: shoalrun export [config] --out DIRECTORY [--target NAME]\n [--format markdown|html|both] [--document ID]...\n [--channel C --version V --language L] [--public-base /help/] [--review]\n [--previous-manifest FILE] [--fail-on-removed]\n\nExport selected pages, sections, and named snippets as Markdown and passive HTML.\nConfigure typed resources in an export target; --document selects whole pages.\nNavigation, facets, and a prior website build are not required.\n--review writes an API-review candidate; status needs-review exits 2 until pinned.\n--previous-manifest compares public references with an earlier reviewed export.\n--fail-on-removed requires that baseline and refuses to replace output if references disappear.\n";
1
+ export declare const exportHelp = "Usage: shoalrun export [config] --out DIRECTORY [--target NAME]\n [--format markdown|html|both] [--document ID]...\n [--channel C --version V --language L] [--public-base /help/] [--review]\n [--profile FILE] [--previous-manifest FILE] [--fail-on-removed]\n\nExport selected pages, sections, and named snippets as Markdown and passive HTML.\nConfigure typed resources in an export target; --document selects whole pages.\nNavigation, facets, and a prior website build are not required.\n--review writes an API-review candidate; status needs-review exits 2 until pinned.\n--previous-manifest compares public references with an earlier reviewed export.\n--fail-on-removed requires that baseline and refuses to replace output if references disappear.\n";
2
2
  export declare function runExportCommand(args: string[]): Promise<{
3
3
  status: string;
4
4
  output: string;
5
5
  publication: import("./content-source.js").Variant;
6
6
  formats: import("./content-export-config.js").ExportFormat[];
7
+ profileSelection: {
8
+ schemaVersion: number;
9
+ profileDigest: string;
10
+ features: string[];
11
+ provenance: Record<string, string[]>;
12
+ decisions: {
13
+ kind: "page" | "section";
14
+ id: string;
15
+ included: boolean;
16
+ requires?: import("./deployment-profile.js").FeatureRequirements;
17
+ }[];
18
+ } | undefined;
7
19
  resources: number;
8
20
  documents: number;
9
21
  documentIds: string[];
@@ -2,7 +2,7 @@ import { exportContent } from './content-export.js';
2
2
  export const exportHelp = `Usage: shoalrun export [config] --out DIRECTORY [--target NAME]
3
3
  [--format markdown|html|both] [--document ID]...
4
4
  [--channel C --version V --language L] [--public-base /help/] [--review]
5
- [--previous-manifest FILE] [--fail-on-removed]
5
+ [--profile FILE] [--previous-manifest FILE] [--fail-on-removed]
6
6
 
7
7
  Export selected pages, sections, and named snippets as Markdown and passive HTML.
8
8
  Configure typed resources in an export target; --document selects whole pages.
@@ -30,7 +30,7 @@ export async function runExportCommand(args) {
30
30
  continue;
31
31
  }
32
32
  if (arg.startsWith('--')) {
33
- if (!['--out', '--target', '--format', '--document', '--channel', '--version', '--language', '--public-base', '--previous-manifest'].includes(arg) || !args[index + 1] || args[index + 1].startsWith('--'))
33
+ if (!['--out', '--target', '--format', '--document', '--channel', '--version', '--language', '--public-base', '--previous-manifest', '--profile'].includes(arg) || !args[index + 1] || args[index + 1].startsWith('--'))
34
34
  throw new Error(exportHelp);
35
35
  const value = args[++index];
36
36
  if (arg === '--document') {
@@ -61,6 +61,7 @@ export async function runExportCommand(args) {
61
61
  throw new Error('Select all three export publication coordinates: --channel, --version, and --language');
62
62
  options.publication = { channel: coordinates[0], version: coordinates[1], language: coordinates[2] };
63
63
  }
64
+ options.profile = values.get('--profile');
64
65
  options.target = values.get('--target');
65
66
  options.publicBase = values.get('--public-base');
66
67
  options.previousManifest = values.get('--previous-manifest');
@@ -1,6 +1,7 @@
1
1
  import { type ExportFormat } from './content-export-config.js';
2
2
  import { type Variant } from './content.js';
3
3
  export type ContentExportOptions = {
4
+ profile?: string;
4
5
  target?: string;
5
6
  formats?: ExportFormat[];
6
7
  documents?: string[];
@@ -16,6 +17,18 @@ export declare function exportContent(configFile: string, destination: string, o
16
17
  output: string;
17
18
  publication: Variant;
18
19
  formats: ExportFormat[];
20
+ profileSelection: {
21
+ schemaVersion: number;
22
+ profileDigest: string;
23
+ features: string[];
24
+ provenance: Record<string, string[]>;
25
+ decisions: {
26
+ kind: "page" | "section";
27
+ id: string;
28
+ included: boolean;
29
+ requires?: import("./deployment-profile.js").FeatureRequirements;
30
+ }[];
31
+ } | undefined;
19
32
  resources: number;
20
33
  documents: number;
21
34
  documentIds: string[];