bmad-plus 0.12.2 → 0.14.0

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 (127) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +96 -461
  3. package/package.json +12 -5
  4. package/readme-international/README.de.md +42 -9
  5. package/readme-international/README.es.md +43 -10
  6. package/readme-international/README.fr.md +42 -9
  7. package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +5 -3
  8. package/src/bmad-plus/module.yaml +270 -220
  9. package/src/bmad-plus/packs/pack-dev-studio/README.md +133 -141
  10. package/src/bmad-plus/packs/pack-dev-studio/SKILL.md +49 -0
  11. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/analyst-agent.md +35 -60
  12. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/document-project.md +59 -59
  13. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/domain-research.md +55 -93
  14. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/market-research.md +58 -93
  15. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/prfaq.md +55 -132
  16. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/product-brief.md +63 -78
  17. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/tech-writer-agent.md +54 -69
  18. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/technical-research.md +54 -93
  19. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/architect-agent.md +32 -60
  20. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-architecture.md +67 -71
  21. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-epics-stories.md +61 -90
  22. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/generate-project-context.md +56 -78
  23. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/implementation-readiness.md +55 -88
  24. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01-init.md +20 -153
  25. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01b-continue.md +20 -173
  26. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-02-context.md +14 -220
  27. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-03-starter.md +20 -329
  28. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-04-decisions.md +15 -314
  29. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-05-patterns.md +15 -355
  30. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-06-structure.md +15 -375
  31. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-07-validation.md +14 -357
  32. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-08-complete.md +13 -78
  33. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/checkpoint-preview.md +52 -65
  34. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-01-gather-context.md +14 -81
  35. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-02-review.md +14 -31
  36. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-03-triage.md +14 -45
  37. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-04-present.md +13 -128
  38. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +61 -87
  39. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/correct-course.md +55 -298
  40. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/create-story.md +52 -426
  41. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-agent.md +48 -69
  42. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story-checklist.md +22 -80
  43. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story.md +60 -482
  44. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/investigate.md +50 -184
  45. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/qa-e2e-tests.md +57 -173
  46. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/quick-dev.md +56 -108
  47. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/retrospective.md +54 -1509
  48. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-planning.md +54 -296
  49. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-status.md +41 -283
  50. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-prd.md +58 -18
  51. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-ux-design.md +103 -72
  52. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/edit-prd.md +55 -27
  53. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/pm-agent.md +34 -60
  54. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/prd.md +46 -87
  55. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/steps/step-01-init.md +10 -0
  56. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/ux-designer-agent.md +30 -60
  57. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/validate-prd.md +57 -27
  58. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/advanced-elicitation.md +47 -138
  59. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/adversarial-review.md +48 -34
  60. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/bmad-help.md +51 -68
  61. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/brainstorming.md +46 -3
  62. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/customize.md +68 -109
  63. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/distillator.md +53 -174
  64. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/edge-case-hunter.md +39 -53
  65. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-prose.md +45 -83
  66. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-structure.md +45 -176
  67. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/index-docs.md +45 -63
  68. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/party-mode.md +53 -124
  69. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/shard-doc.md +44 -100
  70. package/src/bmad-plus/packs/pack-dev-studio/dev-studio-orchestrator.md +56 -116
  71. package/src/bmad-plus/packs/pack-dev-studio/shared/architecture-decision-template.md +49 -12
  72. package/src/bmad-plus/packs/pack-dev-studio/shared/bwml-spec.md +51 -328
  73. package/src/bmad-plus/packs/pack-dev-studio/shared/catalog.json +489 -0
  74. package/src/bmad-plus/packs/pack-dev-studio/shared/execution.md +69 -0
  75. package/src/bmad-plus/packs/pack-dev-studio/shared/module-help.csv +39 -32
  76. package/src/bmad-plus/packs/pack-dev-studio/upstream-sync.yaml +85 -14
  77. package/src/bmad-plus/packs/pack-memory/README.md +6 -0
  78. package/src/bmad-plus/packs/pack-memory/memory-orchestrator.md +12 -5
  79. package/src/bmad-plus/packs/pack-seo/SKILL.md +27 -1
  80. package/src/bmad-plus/packs/pack-seo/scripts/seo_apis.py +8 -8
  81. package/src/bmad-plus/packs/pack-seo/scripts/seo_fetch.py +1 -2
  82. package/src/bmad-plus/packs/pack-seo/scripts/seo_report.py +0 -1
  83. package/src/bmad-plus/packs/pack-seo/seo-chief.md +16 -1
  84. package/src/bmad-plus/packs/pack-seo/seo-judge.md +12 -0
  85. package/src/bmad-plus/packs/pack-seo/seo-scout.md +12 -0
  86. package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +4 -4
  87. package/src/bmad-plus/skills/bmad-plus-sync/SKILL.md +5 -3
  88. package/src/bmad-plus/skills/dev-studio/SKILL.md +19 -0
  89. package/tools/bmad-plus-npx.js +4 -2
  90. package/tools/build/adapters.config.js +60 -51
  91. package/tools/build/check-counts.js +52 -54
  92. package/tools/build/check-install-contract.js +450 -0
  93. package/tools/build/generate-adapters.js +252 -56
  94. package/tools/build/generate.js +364 -14
  95. package/tools/build/generated-adapters/.codex/AGENTS.md +20 -7
  96. package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +20 -7
  97. package/tools/build/generated-adapters/.opencode/AGENTS.md +20 -7
  98. package/tools/build/generated-adapters/AGENTS.md +20 -7
  99. package/tools/build/generated-adapters/CLAUDE.md +20 -7
  100. package/tools/build/generated-adapters/CONVENTIONS.md +20 -7
  101. package/tools/build/generated-adapters/GEMINI.md +20 -7
  102. package/tools/build/module.template.yaml +82 -0
  103. package/tools/build/pack-delivery.js +78 -0
  104. package/tools/cli/bmad-plus-cli.js +16 -1
  105. package/tools/cli/commands/doctor.js +49 -217
  106. package/tools/cli/commands/install.js +109 -164
  107. package/tools/cli/commands/studio.js +68 -0
  108. package/tools/cli/commands/uninstall.js +173 -65
  109. package/tools/cli/commands/update-check.js +31 -0
  110. package/tools/cli/commands/update-policy.js +39 -0
  111. package/tools/cli/commands/update.js +102 -113
  112. package/tools/cli/i18n.js +60 -0
  113. package/tools/cli/lib/ide-config.js +4 -261
  114. package/tools/cli/lib/install-manifest.js +17 -0
  115. package/tools/cli/lib/installation-health.js +360 -0
  116. package/tools/cli/lib/installed-adapters.js +89 -0
  117. package/tools/cli/lib/npm-runner.js +177 -0
  118. package/tools/cli/lib/pack-copy.js +62 -66
  119. package/tools/cli/lib/packs.js +546 -3
  120. package/tools/cli/lib/python-health.js +233 -0
  121. package/tools/cli/lib/python-provision.js +2 -2
  122. package/tools/cli/lib/studio.js +310 -0
  123. package/tools/cli/lib/update-check.js +153 -0
  124. package/tools/cli/lib/update-dispatch.js +182 -0
  125. package/tools/cli/lib/update-policy.js +90 -0
  126. package/tools/cli/lib/update-transaction.js +334 -0
  127. package/tools/release/publication-content.js +901 -0
@@ -0,0 +1,901 @@
1
+ 'use strict';
2
+
3
+ // Editorial inputs stay in the source tree. npm publication is verified separately
4
+ // from README preparation so an unpublished candidate never becomes a public snapshot.
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+ const crypto = require('node:crypto');
9
+ const { spawnSync } = require('node:child_process');
10
+ const yaml = require('js-yaml');
11
+ const semver = require('semver');
12
+ const { TOOL_METADATA } = require('../build/adapters.config');
13
+
14
+ const REGISTRY_URL = 'https://registry.npmjs.org/bmad-plus';
15
+ const SITE_URL = 'https://bmad-plus.rochetta.fr';
16
+ const LOCALES = ['en', 'fr', 'es', 'de'];
17
+ const MODES = [
18
+ 'write-readme',
19
+ 'check',
20
+ 'validate-candidate',
21
+ 'verify-history',
22
+ 'verify-unpublished',
23
+ 'sync-published',
24
+ ];
25
+ const MAX_METADATA_BYTES = 8 * 1024 * 1024;
26
+ const REQUEST_TIMEOUT_MS = 15000;
27
+ const CANDIDATE_SITE_URL = 'https://bmad-plus-candidate-synthetic.invalid';
28
+ const SYNTHETIC_FIXTURE_NOTICE =
29
+ 'SYNTHETIC FIXTURES: candidate rendering only; not publication evidence; never deploy.';
30
+ const CANDIDATE_NODE_TEST_FILES = Object.freeze(['check-release.test.js', 'check-search.test.js']);
31
+ const CANDIDATE_WEBSITE_FILES = Object.freeze([
32
+ 'build.js',
33
+ 'check-release.js',
34
+ 'check-search.js',
35
+ 'content.js',
36
+ 'docs-content.js',
37
+ 'search-assets.js',
38
+ 'docs-page.js',
39
+ 'docs-markdown.js',
40
+ 'page-shell.js',
41
+ 'styles.css',
42
+ 'site.js',
43
+ 'favicon.svg',
44
+ 'share-card.png',
45
+ '_headers',
46
+ ...CANDIDATE_NODE_TEST_FILES,
47
+ ]);
48
+
49
+ // Run the unmodified source renderer in its own process and disposable filesystem.
50
+ // Do not return paths or publication-shaped data to callers. The extra payload
51
+ // marker makes check-search reject accidental reuse of the validation output.
52
+ const CANDIDATE_CHECK_SCRIPT = String.raw`
53
+ 'use strict';
54
+ const fs = require('node:fs');
55
+ const path = require('node:path');
56
+ console.log = () => {};
57
+ console.warn = () => {};
58
+ try {
59
+ const snapshot = require('./website/release.json');
60
+ if (!snapshot.syntheticFixtures || snapshot.deployable !== false ||
61
+ snapshot.publicationVerified !== false || snapshot.version !== require('./package.json').version) {
62
+ throw new Error('Candidate validation requires an explicitly synthetic snapshot of this package.');
63
+ }
64
+ const { checkRelease } = require('./website/check-release');
65
+ const release = checkRelease();
66
+ if (!release.ok) throw new Error(release.errors.slice(0, 12).join('\n'));
67
+ const documentation = require('./website/docs-content');
68
+ for (const [locale, copy] of Object.entries(documentation)) {
69
+ for (const step of documentation.en.steps) {
70
+ const translated = copy.steps.find(item => item.id === step.id);
71
+ if (translated.type !== step.type ||
72
+ (step.type === 'terminal' && translated.code !== step.code)) {
73
+ throw new Error(locale + ': translated command type or code differs for ' + step.id + '.');
74
+ }
75
+ }
76
+ }
77
+ const built = require('./website/build').build({ siteUrl: process.env.SITE_URL });
78
+ if (!built.report.ok) throw new Error(built.report.errors.slice(0, 12).join('\n'));
79
+ fs.writeFileSync(path.join(built.outDir, 'SYNTHETIC-VALIDATION-NEVER-DEPLOY.txt'), snapshot.fixtureNotice + '\n');
80
+ fs.appendFileSync(path.join(built.outDir, '_headers'), '\n/*\n X-Robots-Tag: noindex, nofollow\n X-BMAD-Validation: synthetic-fixtures-never-deploy\n');
81
+ process.stdout.write(JSON.stringify({ ok: true, releaseChecks: release.checks,
82
+ searchChecks: built.report.checks, pages: built.report.pages }));
83
+ } catch (error) {
84
+ process.stdout.write(JSON.stringify({ ok: false, message: String(error.message).slice(0, 6000) }));
85
+ process.exitCode = 1;
86
+ }
87
+ `;
88
+
89
+ function requireCondition(condition, message) {
90
+ if (!condition) throw new Error(message);
91
+ }
92
+
93
+ function nonempty(value) {
94
+ return typeof value === 'string' && value.trim().length > 0;
95
+ }
96
+
97
+ function normalizeNewlines(text) {
98
+ return text.replace(/\r\n?/g, '\n');
99
+ }
100
+
101
+ function stableVersion(value) {
102
+ return (
103
+ typeof value === 'string' && /^\d+\.\d+\.\d+$/.test(value) && semver.valid(value) === value
104
+ );
105
+ }
106
+
107
+ function validDate(value) {
108
+ return (
109
+ typeof value === 'string' &&
110
+ /^\d{4}-\d{2}-\d{2}$/.test(value) &&
111
+ Number.isFinite(Date.parse(value)) &&
112
+ new Date(value).toISOString().slice(0, 10) === value
113
+ );
114
+ }
115
+
116
+ function sameList(left, right) {
117
+ return JSON.stringify(left) === JSON.stringify(right);
118
+ }
119
+
120
+ function changelogEntry(changelog, version) {
121
+ requireCondition(
122
+ typeof changelog === 'string' && stableVersion(version),
123
+ 'Invalid CHANGELOG input.'
124
+ );
125
+ const escaped = version.replace(/\./g, '\\.');
126
+ const pattern = new RegExp(
127
+ `^## \\[${escaped}\\][^\\n]*(?:\\n[\\s\\S]*?)?(?=^## |$(?![\\s\\S]))`,
128
+ 'gm'
129
+ );
130
+ const entries = normalizeNewlines(changelog).match(pattern) || [];
131
+ requireCondition(entries.length <= 1, `${version}: duplicate CHANGELOG entries.`);
132
+ return entries[0]?.trim() || '';
133
+ }
134
+
135
+ function notesHash(entry) {
136
+ return crypto.createHash('sha256').update(entry).digest('hex');
137
+ }
138
+
139
+ function completeSummary(summary) {
140
+ return (
141
+ summary &&
142
+ nonempty(summary.title) &&
143
+ nonempty(summary.summary) &&
144
+ Array.isArray(summary.items) &&
145
+ summary.items.length > 0 &&
146
+ summary.items.every(nonempty)
147
+ );
148
+ }
149
+
150
+ function reviewedNotes(documentation, changelog, version) {
151
+ if (
152
+ !LOCALES.every((locale) => completeSummary(documentation[locale]?.releaseChanges?.[version]))
153
+ ) {
154
+ return null;
155
+ }
156
+ const itemCount = documentation.en.releaseChanges[version].items.length;
157
+ if (
158
+ !LOCALES.every(
159
+ (locale) => documentation[locale].releaseChanges[version].items.length === itemCount
160
+ )
161
+ ) {
162
+ return null;
163
+ }
164
+ const entry = changelogEntry(changelog, version);
165
+ if (
166
+ !entry ||
167
+ !entry.includes('\n') ||
168
+ !entry
169
+ .slice(entry.indexOf('\n') + 1)
170
+ .replace(/^###.*$/gm, '')
171
+ .trim()
172
+ ) {
173
+ return null;
174
+ }
175
+ const notesDate = entry
176
+ .split('\n')[0]
177
+ .match(/^## \[[^\]]+\]\s+[-—]\s+(\d{4}-\d{2}-\d{2})\s*$/)?.[1];
178
+ requireCondition(validDate(notesDate), `${version}: a real CHANGELOG notes date is required.`);
179
+ return { version, notesDate, notesSha256: notesHash(entry) };
180
+ }
181
+
182
+ function selectReviewedReleases({ version, documentation, changelog }) {
183
+ requireCondition(stableVersion(version), 'The candidate must have an exact stable version.');
184
+ requireCondition(
185
+ LOCALES.every((locale) => documentation?.[locale]?.releaseChanges),
186
+ 'Reviewed release summaries are required in English, French, Spanish and German.'
187
+ );
188
+ requireCondition(
189
+ reviewedNotes(documentation, changelog, version),
190
+ `${version}: a CHANGELOG entry and complete, matching summaries in all four languages are required.`
191
+ );
192
+ const versions = Object.keys(documentation.en.releaseChanges)
193
+ .filter((item) => stableVersion(item) && semver.lte(item, version))
194
+ .sort(semver.rcompare);
195
+ const releases = [];
196
+ for (const item of versions) {
197
+ const notes = reviewedNotes(documentation, changelog, item);
198
+ if (notes) releases.push(notes);
199
+ if (releases.length === 3) break;
200
+ }
201
+ requireCondition(
202
+ releases.length === 3 && releases[0].version === version,
203
+ 'Three reviewed releases with CHANGELOG entries are required, starting with the candidate.'
204
+ );
205
+ return releases;
206
+ }
207
+
208
+ function licenseFooter(sourceReadme) {
209
+ requireCondition(
210
+ typeof sourceReadme === 'string',
211
+ 'The source README with license credits is required.'
212
+ );
213
+ const normalized = normalizeNewlines(sourceReadme);
214
+ const matches = [...normalized.matchAll(/^## [^\n]*\bLicense\s*$/gm)];
215
+ requireCondition(matches.length === 1, 'The source README must contain one License footer.');
216
+ const footer = normalized.slice(matches[0].index).trimEnd();
217
+ requireCondition(
218
+ /^### Credits\s*$/m.test(footer),
219
+ 'The source README license credits are missing.'
220
+ );
221
+ return footer;
222
+ }
223
+
224
+ function tableCell(text) {
225
+ return text.replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
226
+ }
227
+
228
+ function codeBlock(code, type) {
229
+ const longest = Math.max(2, ...(code.match(/`+/g) || []).map((run) => run.length));
230
+ const fence = '`'.repeat(longest + 1);
231
+ return `${fence}${type === 'terminal' ? 'sh' : 'text'}\n${code}\n${fence}`;
232
+ }
233
+
234
+ function preparePublication({
235
+ pkg,
236
+ registry,
237
+ landing,
238
+ documentation,
239
+ changelog,
240
+ sourceReadme,
241
+ release,
242
+ toolMetadata = TOOL_METADATA,
243
+ }) {
244
+ requireCondition(
245
+ pkg?.name === 'bmad-plus' && stableVersion(pkg.version),
246
+ 'Publication requires the bmad-plus package with an exact stable version.'
247
+ );
248
+ requireCondition(
249
+ registry?.product?.version === pkg.version && registry.product.code === pkg.name,
250
+ 'package.json and registry.product must describe the same package and version.'
251
+ );
252
+ requireCondition(
253
+ nonempty(pkg.description) &&
254
+ nonempty(pkg.license) &&
255
+ nonempty(pkg.engines?.node) &&
256
+ Object.values(pkg.engines).every(nonempty) &&
257
+ semver.validRange(pkg.engines.node),
258
+ 'The package description, license and Node.js engine must be explicit.'
259
+ );
260
+ requireCondition(
261
+ semver.minVersion(pkg.engines.node)?.version === registry.runtimes?.node?.min_version,
262
+ 'The package Node.js minimum differs from the registry runtime.'
263
+ );
264
+ requireCondition(
265
+ registry.targets?.integration?.execution === 'host-managed' &&
266
+ registry.targets.integration.lifecycle_events === 'not-integrated',
267
+ 'Review the execution wording before publishing a different host integration contract.'
268
+ );
269
+ requireCondition(registry.targets.spine === 'AGENTS.md', 'Review the documented project spine.');
270
+
271
+ const packEntries = Object.values(registry.packs || {});
272
+ requireCondition(
273
+ packEntries.length > 0 &&
274
+ packEntries.every(
275
+ (pack) => Number.isInteger(pack.order) && pack.order >= 0 && nonempty(pack.cli?.name)
276
+ ) &&
277
+ new Set(packEntries.map((pack) => pack.order)).size === packEntries.length,
278
+ 'Registry packs need unique ordering and explicit CLI names.'
279
+ );
280
+ const packs = [...packEntries].sort((a, b) => a.order - b.order).map((pack) => pack.cli.name);
281
+ requireCondition(new Set(packs).size === packs.length, 'Registry pack names must be unique.');
282
+ const personas = registry.packs.core?.personas?.map((persona) => persona.name);
283
+ requireCondition(
284
+ personas?.length > 0 && personas.every(nonempty),
285
+ 'Core persona names are missing.'
286
+ );
287
+ for (const locale of LOCALES) {
288
+ const copy = landing?.[locale];
289
+ requireCondition(
290
+ Array.isArray(copy?.packs) &&
291
+ copy.packs.every(
292
+ (pack) => Array.isArray(pack) && pack.length === 2 && pack.every(nonempty)
293
+ ) &&
294
+ sameList(
295
+ copy.packs.map(([name]) => name),
296
+ packs
297
+ ),
298
+ `${locale}: landing packs differ from the registry.`
299
+ );
300
+ requireCondition(
301
+ Array.isArray(copy.roles) &&
302
+ sameList(
303
+ copy.roles.map((role) => role.name),
304
+ personas
305
+ ) &&
306
+ copy.roles.every((role) => nonempty(role.job) && nonempty(role.summary)),
307
+ `${locale}: landing roles differ from the registry.`
308
+ );
309
+ }
310
+ const adapters = registry.targets.adapters;
311
+ requireCondition(
312
+ Array.isArray(adapters) &&
313
+ adapters.length > 0 &&
314
+ adapters.every(
315
+ (adapter) =>
316
+ nonempty(adapter.tool) &&
317
+ nonempty(adapter.file) &&
318
+ Object.hasOwn(toolMetadata, adapter.tool) &&
319
+ nonempty(toolMetadata[adapter.tool]?.name)
320
+ ) &&
321
+ new Set(adapters.map((adapter) => adapter.tool)).size === adapters.length,
322
+ 'Registry targets must have unique, recognized adapters with explicit file paths.'
323
+ );
324
+ const tools = adapters.map((adapter) => toolMetadata[adapter.tool].name);
325
+ requireCondition(new Set(tools).size === tools.length, 'Adapter display names must be unique.');
326
+ requireCondition(
327
+ release?.schemaVersion === 1 && validDate(release.reviewedAt),
328
+ 'Keep an explicit, real reviewedAt date in website/release.json.'
329
+ );
330
+ const releases = selectReviewedReleases({ version: pkg.version, documentation, changelog });
331
+ requireCondition(
332
+ release.reviewedAt >= releases[0].notesDate,
333
+ 'The explicit documentation review date predates the candidate release notes.'
334
+ );
335
+
336
+ const docs = documentation.en;
337
+ requireCondition(
338
+ Array.isArray(docs.steps) &&
339
+ docs.steps.length > 0 &&
340
+ docs.steps.every(
341
+ (step) =>
342
+ nonempty(step.id) &&
343
+ nonempty(step.title) &&
344
+ nonempty(step.body) &&
345
+ nonempty(step.code) &&
346
+ nonempty(step.outcome) &&
347
+ ['terminal', 'assistant'].includes(step.type)
348
+ ) &&
349
+ new Set(docs.steps.map((step) => step.id)).size === docs.steps.length,
350
+ 'English documentation needs complete, distinct installation steps.'
351
+ );
352
+ requireCondition(
353
+ docs.steps.find((step) => step.id === 'install')?.code === 'npx bmad-plus@{version} install' &&
354
+ docs.steps.find((step) => step.id === 'install').type === 'terminal',
355
+ 'The installation guide must use the explicitly versioned package command.'
356
+ );
357
+ requireCondition(
358
+ Array.isArray(docs.examples) &&
359
+ docs.examples.length > 0 &&
360
+ docs.examples.every(
361
+ (example) =>
362
+ nonempty(example.id) &&
363
+ nonempty(example.title) &&
364
+ nonempty(example.context) &&
365
+ nonempty(example.prompt) &&
366
+ nonempty(example.result)
367
+ ) &&
368
+ new Set(docs.examples.map((example) => example.id)).size === docs.examples.length,
369
+ 'English documentation needs complete, distinct examples.'
370
+ );
371
+
372
+ const resolve = (text) => text.replaceAll('{version}', pkg.version);
373
+ const lines = [
374
+ '# BMAD+',
375
+ '',
376
+ `[![Version](https://img.shields.io/badge/version-${pkg.version}-blue)](https://www.npmjs.com/package/bmad-plus)`,
377
+ '',
378
+ `**Version ${pkg.version}** · Node.js \`${pkg.engines.node}\` · ${pkg.license}`,
379
+ '',
380
+ pkg.description,
381
+ '',
382
+ `[Website](${SITE_URL}/) · [Get started](${SITE_URL}/docs/#start) · [Examples](${SITE_URL}/docs/#examples) · [What’s new](${SITE_URL}/docs/#news)`,
383
+ '',
384
+ 'BMAD+ installs project instructions, roles and workflows for your existing AI coding tool. Execution is host-managed: your tool supplies the model, permissions, command execution and any parallel-agent capability. BMAD+ does not provide a standalone agent runtime or integrated host lifecycle hooks. Model subscriptions and API access are separate.',
385
+ '',
386
+ `Adapters: ${tools.join(', ')}.`,
387
+ '',
388
+ '## Get started',
389
+ '',
390
+ `Requires Node.js \`${pkg.engines.node}\`. Optional packs may need additional runtimes or API access. Run terminal commands from your project folder.`,
391
+ '',
392
+ ];
393
+ docs.steps.forEach((step, index) => {
394
+ lines.push(
395
+ `### ${index + 1}. ${step.title}`,
396
+ '',
397
+ resolve(step.body),
398
+ '',
399
+ step.type === 'terminal' ? '**In your project terminal:**' : '**In your AI assistant:**',
400
+ '',
401
+ codeBlock(resolve(step.code), step.type),
402
+ '',
403
+ resolve(step.outcome),
404
+ ''
405
+ );
406
+ });
407
+ lines.push('## Core roles', '', '| Role | Focus | Purpose |', '| --- | --- | --- |');
408
+ for (const role of landing.en.roles) {
409
+ lines.push(`| ${tableCell(role.name)} | ${tableCell(role.job)} | ${tableCell(role.summary)} |`);
410
+ }
411
+ lines.push('', '## Packs', '', '| Pack | What it provides |', '| --- | --- |');
412
+ for (const [name, description] of landing.en.packs) {
413
+ lines.push(`| ${tableCell(name)} | ${tableCell(description)} |`);
414
+ }
415
+ lines.push('', '## Examples', '');
416
+ for (const example of docs.examples) {
417
+ lines.push(
418
+ `### ${example.title}`,
419
+ '',
420
+ resolve(example.context),
421
+ '',
422
+ codeBlock(resolve(example.prompt), 'assistant'),
423
+ '',
424
+ resolve(example.result),
425
+ ''
426
+ );
427
+ }
428
+ const current = docs.releaseChanges[pkg.version];
429
+ lines.push(
430
+ `## What’s new in ${pkg.version}`,
431
+ '',
432
+ current.title,
433
+ '',
434
+ current.summary,
435
+ '',
436
+ ...current.items.map((item) => `- ${item}`),
437
+ '',
438
+ '## Version History',
439
+ '',
440
+ 'These dates identify reviewed CHANGELOG notes, not npm publication dates. The website verifies npm publication dates separately.',
441
+ '',
442
+ '| Version | Release-notes date | Reviewed summary |',
443
+ '| --- | --- | --- |'
444
+ );
445
+ for (const item of releases) {
446
+ lines.push(
447
+ `| ${item.version} | ${item.notesDate} | ${tableCell(docs.releaseChanges[item.version].summary)} |`
448
+ );
449
+ }
450
+ lines.push(
451
+ '',
452
+ `[Release history and update guide](${SITE_URL}/docs/#changelog) · [All published npm versions](https://www.npmjs.com/package/bmad-plus?activeTab=versions)`,
453
+ '',
454
+ '---',
455
+ '',
456
+ licenseFooter(sourceReadme),
457
+ ''
458
+ );
459
+ return {
460
+ name: pkg.name,
461
+ version: pkg.version,
462
+ description: pkg.description,
463
+ engines: { ...pkg.engines },
464
+ nodeEngine: pkg.engines.node,
465
+ license: pkg.license,
466
+ reviewedAt: release.reviewedAt,
467
+ packs,
468
+ tools,
469
+ releases,
470
+ readme: lines.join('\n'),
471
+ };
472
+ }
473
+
474
+ function generateReadme(sources) {
475
+ return preparePublication(sources).readme;
476
+ }
477
+
478
+ function publicationDate(metadata, version) {
479
+ requireCondition(
480
+ metadata?.versions?.[version]?.version === version,
481
+ `${version}: the version is not present in official npm metadata.`
482
+ );
483
+ const publishedAt = metadata.time?.[version];
484
+ requireCondition(
485
+ typeof publishedAt === 'string' &&
486
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(publishedAt) &&
487
+ Number.isFinite(Date.parse(publishedAt)) &&
488
+ new Date(publishedAt).toISOString() === publishedAt,
489
+ `${version}: a real npm publication timestamp is required.`
490
+ );
491
+ return publishedAt;
492
+ }
493
+
494
+ function verifyHistory({ publication, metadata }) {
495
+ requireCondition(
496
+ metadata?.name === publication.name,
497
+ 'npm metadata describes a different package.'
498
+ );
499
+ const latest = metadata?.['dist-tags']?.latest;
500
+ requireCondition(
501
+ stableVersion(latest) && metadata.versions?.[latest]?.version === latest,
502
+ 'npm latest is unknown or is not a published stable release.'
503
+ );
504
+ requireCondition(
505
+ semver.gte(publication.version, latest),
506
+ 'The candidate is older than npm latest; refusing a stale release.'
507
+ );
508
+ const history = publication.releases.filter((item) => item.version !== publication.version);
509
+ for (const item of history) publicationDate(metadata, item.version);
510
+ return { latestVersion: latest, verifiedVersions: history.map((item) => item.version) };
511
+ }
512
+
513
+ function verifyUnpublished({ publication, metadata }) {
514
+ requireCondition(
515
+ metadata?.name === publication.name &&
516
+ metadata.versions &&
517
+ typeof metadata.versions === 'object' &&
518
+ !Array.isArray(metadata.versions),
519
+ 'npm publication metadata is unknown.'
520
+ );
521
+ if (Object.hasOwn(metadata.versions, publication.version)) {
522
+ const error = new Error(
523
+ `${publication.version} is already published on npm. Use landing_only recovery; never republish this version.`
524
+ );
525
+ error.code = 'ALREADY_PUBLISHED';
526
+ error.recovery = 'landing_only';
527
+ throw error;
528
+ }
529
+ return { ...verifyHistory({ publication, metadata }), candidateUnpublished: true };
530
+ }
531
+
532
+ function syntheticCandidateSnapshot(publication) {
533
+ return {
534
+ schemaVersion: 1,
535
+ version: publication.version,
536
+ reviewedAt: publication.reviewedAt,
537
+ nodeEngine: publication.nodeEngine,
538
+ license: publication.license,
539
+ packs: [...publication.packs],
540
+ tools: [...publication.tools],
541
+ releases: publication.releases.map((item, index) => ({
542
+ version: item.version,
543
+ // Deliberately fixed fixture timestamps; they are not inferred publication dates.
544
+ publishedAt: new Date(Date.UTC(2000, 0, 3 - index)).toISOString(),
545
+ notesDate: item.notesDate,
546
+ notesSha256: item.notesSha256,
547
+ })),
548
+ syntheticFixtures: true,
549
+ publicationVerified: false,
550
+ deployable: false,
551
+ fixtureNotice: SYNTHETIC_FIXTURE_NOTICE,
552
+ };
553
+ }
554
+
555
+ function validateCandidate({ rootDir, publication, runner = spawnSync }) {
556
+ const parent = fs.realpathSync(os.tmpdir());
557
+ const prefix = 'bmad-candidate-synthetic-';
558
+ const temporary = fs.mkdtempSync(path.join(parent, prefix));
559
+ try {
560
+ const website = path.join(rootDir, 'website');
561
+ requireCondition(
562
+ fs.lstatSync(website).isDirectory() && !fs.lstatSync(website).isSymbolicLink(),
563
+ 'Candidate validation requires a real website source directory.'
564
+ );
565
+ fs.mkdirSync(path.join(temporary, 'website'));
566
+ const files = [
567
+ 'package.json',
568
+ 'CHANGELOG.md',
569
+ ...CANDIDATE_WEBSITE_FILES.map((file) => path.join('website', file)),
570
+ ];
571
+ for (const file of files) {
572
+ const source = path.join(rootDir, file);
573
+ const stat = fs.lstatSync(source);
574
+ requireCondition(
575
+ stat.isFile() && !stat.isSymbolicLink(),
576
+ `Candidate source must be a regular file: ${file}.`
577
+ );
578
+ fs.copyFileSync(source, path.join(temporary, file));
579
+ }
580
+ const snapshot = syntheticCandidateSnapshot(publication);
581
+ fs.writeFileSync(
582
+ path.join(temporary, 'website/release.json'),
583
+ `${JSON.stringify(snapshot, null, 2)}\n`
584
+ );
585
+ fs.writeFileSync(
586
+ path.join(temporary, 'SYNTHETIC-FIXTURES-NEVER-DEPLOY.txt'),
587
+ `${SYNTHETIC_FIXTURE_NOTICE}\n`
588
+ );
589
+ const processOptions = {
590
+ cwd: temporary,
591
+ encoding: 'utf8',
592
+ timeout: 30000,
593
+ maxBuffer: 1024 * 1024,
594
+ shell: false,
595
+ windowsHide: true,
596
+ killSignal: 'SIGKILL',
597
+ env: { ...process.env, NODE_OPTIONS: '', NODE_PATH: '', SITE_URL: CANDIDATE_SITE_URL },
598
+ };
599
+ const result = runner(process.execPath, ['-e', CANDIDATE_CHECK_SCRIPT], processOptions);
600
+ let report;
601
+ try {
602
+ report = JSON.parse(result.stdout);
603
+ } catch {
604
+ throw new Error(
605
+ 'Candidate site validation did not return a valid result; no publication is verified.'
606
+ );
607
+ }
608
+ requireCondition(
609
+ !result.error && !result.signal,
610
+ 'Candidate site validation could not complete within the process limits.'
611
+ );
612
+ requireCondition(
613
+ result.status === 0 && report.ok === true,
614
+ `Candidate site validation failed: ${typeof report.message === 'string' ? report.message.slice(0, 6000) : 'invalid validation result'}`
615
+ );
616
+ requireCondition(
617
+ Number.isInteger(report.releaseChecks) &&
618
+ report.releaseChecks > 0 &&
619
+ Number.isInteger(report.searchChecks) &&
620
+ report.searchChecks > 0 &&
621
+ report.pages === LOCALES.length * 2,
622
+ 'Candidate validation did not verify all locale pages.'
623
+ );
624
+ let testResult;
625
+ try {
626
+ testResult = runner(
627
+ process.execPath,
628
+ [
629
+ '--test',
630
+ '--test-reporter=tap',
631
+ ...CANDIDATE_NODE_TEST_FILES.map((file) => path.join('website', file)),
632
+ ],
633
+ processOptions
634
+ );
635
+ } catch {
636
+ throw new Error('Candidate website Node tests could not start; no publication is verified.');
637
+ }
638
+ requireCondition(
639
+ testResult && !testResult.error && !testResult.signal,
640
+ 'Candidate website Node tests could not complete within the process limits.'
641
+ );
642
+ requireCondition(
643
+ testResult.status === 0,
644
+ 'Candidate website Node tests failed before publication (check-release.test.js, check-search.test.js).'
645
+ );
646
+ const count = (label) =>
647
+ Number(testResult.stdout?.match(new RegExp(`^# ${label} (\\d+)\\r?$`, 'm'))?.[1]);
648
+ const websiteTests = count('tests');
649
+ requireCondition(
650
+ Number.isInteger(websiteTests) &&
651
+ websiteTests > 0 &&
652
+ count('pass') === websiteTests &&
653
+ count('fail') === 0 &&
654
+ count('cancelled') === 0 &&
655
+ count('skipped') === 0 &&
656
+ count('todo') === 0,
657
+ 'Candidate website Node tests did not confirm a complete passing test run.'
658
+ );
659
+ return {
660
+ syntheticFixtures: true,
661
+ publicationVerified: false,
662
+ deployable: false,
663
+ artifactsRetained: false,
664
+ message: SYNTHETIC_FIXTURE_NOTICE,
665
+ releaseChecks: report.releaseChecks,
666
+ searchChecks: report.searchChecks,
667
+ pages: report.pages,
668
+ websiteTests,
669
+ };
670
+ } finally {
671
+ // Verify the exact absolute mkdtemp target before a recursive Windows removal.
672
+ requireCondition(
673
+ path.dirname(temporary) === parent &&
674
+ path.basename(temporary).startsWith(prefix) &&
675
+ !fs.lstatSync(temporary).isSymbolicLink(),
676
+ 'Refusing an unexpected candidate cleanup target.'
677
+ );
678
+ fs.rmSync(temporary, { recursive: true, force: true });
679
+ }
680
+ }
681
+
682
+ function buildPublishedSnapshot({ publication, metadata }) {
683
+ verifyHistory({ publication, metadata });
684
+ requireCondition(
685
+ metadata['dist-tags'].latest === publication.version,
686
+ 'The candidate must equal npm latest before the website can be synchronized.'
687
+ );
688
+ const published = metadata.versions[publication.version];
689
+ const sortedEntries = (object) =>
690
+ Object.entries(object || {}).sort(([a], [b]) => a.localeCompare(b));
691
+ requireCondition(
692
+ sameList(sortedEntries(published.engines), sortedEntries(publication.engines)) &&
693
+ published.license === publication.license &&
694
+ published.description === publication.description,
695
+ 'The npm engines, license or description differ from the candidate package.'
696
+ );
697
+ requireCondition(
698
+ typeof metadata.readme === 'string' &&
699
+ normalizeNewlines(metadata.readme) === normalizeNewlines(publication.readme),
700
+ 'The published npm README is missing or stale; website synchronization was refused.'
701
+ );
702
+ let previous = Infinity;
703
+ const releases = publication.releases.map((item) => {
704
+ const publishedAt = publicationDate(metadata, item.version);
705
+ requireCondition(
706
+ Date.parse(publishedAt) < previous,
707
+ 'Review the selected history: npm publication dates must be newest first.'
708
+ );
709
+ previous = Date.parse(publishedAt);
710
+ return {
711
+ version: item.version,
712
+ publishedAt,
713
+ notesDate: item.notesDate,
714
+ notesSha256: item.notesSha256,
715
+ };
716
+ });
717
+ return {
718
+ schemaVersion: 1,
719
+ version: publication.version,
720
+ reviewedAt: publication.reviewedAt,
721
+ nodeEngine: publication.nodeEngine,
722
+ license: publication.license,
723
+ packs: [...publication.packs],
724
+ tools: [...publication.tools],
725
+ releases,
726
+ };
727
+ }
728
+
729
+ async function fetchOfficialMetadata() {
730
+ try {
731
+ const response = await globalThis.fetch(REGISTRY_URL, {
732
+ redirect: 'error',
733
+ signal: globalThis.AbortSignal.timeout(REQUEST_TIMEOUT_MS),
734
+ headers: { Accept: 'application/json' },
735
+ });
736
+ requireCondition(response.ok && response.body, 'npm metadata request failed.');
737
+ const length = response.headers.get('content-length');
738
+ requireCondition(
739
+ length === null || (/^\d+$/.test(length) && Number(length) <= MAX_METADATA_BYTES),
740
+ 'npm metadata exceeds the response limit.'
741
+ );
742
+ const reader = response.body.getReader();
743
+ const chunks = [];
744
+ let size = 0;
745
+ try {
746
+ while (true) {
747
+ const { value, done } = await reader.read();
748
+ if (done) break;
749
+ size += value.byteLength;
750
+ requireCondition(size <= MAX_METADATA_BYTES, 'npm metadata exceeds the response limit.');
751
+ chunks.push(Buffer.from(value));
752
+ }
753
+ } finally {
754
+ await reader.cancel().catch(() => {});
755
+ reader.releaseLock();
756
+ }
757
+ return JSON.parse(Buffer.concat(chunks, size).toString('utf8'));
758
+ } catch {
759
+ // Neither remote response bodies nor environment credentials belong in errors.
760
+ throw new Error('Official npm metadata could not be read within the request limits.');
761
+ }
762
+ }
763
+
764
+ function readSources(rootDir) {
765
+ const read = (name) => fs.readFileSync(path.join(rootDir, name), 'utf8');
766
+ const freshModule = (name) => {
767
+ const filename = require.resolve(path.join(rootDir, name));
768
+ delete require.cache[filename];
769
+ return require(filename);
770
+ };
771
+ return {
772
+ pkg: JSON.parse(read('package.json')),
773
+ registry: yaml.load(read('registry.yaml')),
774
+ landing: freshModule('website/content.js'),
775
+ documentation: freshModule('website/docs-content.js'),
776
+ sourceReadme: read('README.md'),
777
+ changelog: read('CHANGELOG.md'),
778
+ release: JSON.parse(read('website/release.json')),
779
+ };
780
+ }
781
+
782
+ function replaceFile(filename, content) {
783
+ requireCondition(
784
+ !fs.lstatSync(path.dirname(filename)).isSymbolicLink(),
785
+ 'Refusing a redirected publication directory.'
786
+ );
787
+ if (fs.existsSync(filename)) {
788
+ requireCondition(
789
+ fs.lstatSync(filename).isFile() && !fs.lstatSync(filename).isSymbolicLink(),
790
+ 'Refusing a redirected publication file.'
791
+ );
792
+ }
793
+ const temporary = `${filename}.${crypto.randomUUID()}.tmp`;
794
+ try {
795
+ fs.writeFileSync(temporary, content, { encoding: 'utf8', flag: 'wx' });
796
+ fs.renameSync(temporary, filename);
797
+ } finally {
798
+ if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
799
+ }
800
+ }
801
+
802
+ async function runPublicationContent({
803
+ mode,
804
+ rootDir = path.resolve(__dirname, '../..'),
805
+ getMetadata = fetchOfficialMetadata,
806
+ } = {}) {
807
+ requireCondition(
808
+ MODES.includes(mode),
809
+ `Choose exactly one mode: ${MODES.map((item) => `--${item}`).join(', ')}.`
810
+ );
811
+ const publication = preparePublication(readSources(rootDir));
812
+ const readmePath = path.join(rootDir, 'README-DIST.md');
813
+ const existing = fs.existsSync(readmePath) ? fs.readFileSync(readmePath, 'utf8') : null;
814
+ const matches =
815
+ existing !== null && normalizeNewlines(existing) === normalizeNewlines(publication.readme);
816
+ if (mode === 'write-readme') {
817
+ if (!matches) replaceFile(readmePath, publication.readme);
818
+ return { ok: true, mode, version: publication.version, changed: !matches };
819
+ }
820
+ requireCondition(
821
+ matches,
822
+ 'README-DIST.md is stale or missing. Run --write-readme and review the generated content.'
823
+ );
824
+ if (mode === 'check') return { ok: true, mode, version: publication.version, changed: false };
825
+ if (mode === 'validate-candidate') {
826
+ return {
827
+ ok: true,
828
+ mode,
829
+ version: publication.version,
830
+ changed: false,
831
+ ...validateCandidate({ rootDir, publication }),
832
+ };
833
+ }
834
+ let metadata;
835
+ try {
836
+ metadata = await getMetadata();
837
+ } catch {
838
+ throw new Error('Official npm publication could not be verified; no files were changed.');
839
+ }
840
+ if (mode === 'verify-history') {
841
+ return {
842
+ ok: true,
843
+ mode,
844
+ version: publication.version,
845
+ changed: false,
846
+ ...verifyHistory({ publication, metadata }),
847
+ };
848
+ }
849
+ if (mode === 'verify-unpublished') {
850
+ return {
851
+ ok: true,
852
+ mode,
853
+ version: publication.version,
854
+ changed: false,
855
+ ...verifyUnpublished({ publication, metadata }),
856
+ };
857
+ }
858
+ const snapshot = buildPublishedSnapshot({ publication, metadata });
859
+ const target = path.join(rootDir, 'website/release.json');
860
+ const content = `${JSON.stringify(snapshot, null, 2)}\n`;
861
+ const changed = normalizeNewlines(fs.readFileSync(target, 'utf8')) !== content;
862
+ if (changed) replaceFile(target, content);
863
+ return { ok: true, mode, version: publication.version, changed };
864
+ }
865
+
866
+ if (require.main === module) {
867
+ const args = process.argv.slice(2);
868
+ runPublicationContent({
869
+ mode: args.length === 1 && args[0].startsWith('--') ? args[0].slice(2) : null,
870
+ })
871
+ .then((result) => console.log(JSON.stringify(result, null, 2)))
872
+ .catch((error) => {
873
+ console.error(error.message);
874
+ process.exitCode = 1;
875
+ });
876
+ }
877
+
878
+ module.exports = {
879
+ REGISTRY_URL,
880
+ SITE_URL,
881
+ MAX_METADATA_BYTES,
882
+ REQUEST_TIMEOUT_MS,
883
+ CANDIDATE_WEBSITE_FILES,
884
+ CANDIDATE_NODE_TEST_FILES,
885
+ CANDIDATE_SITE_URL,
886
+ SYNTHETIC_FIXTURE_NOTICE,
887
+ normalizeNewlines,
888
+ changelogEntry,
889
+ notesHash,
890
+ licenseFooter,
891
+ selectReviewedReleases,
892
+ preparePublication,
893
+ generateReadme,
894
+ verifyHistory,
895
+ verifyUnpublished,
896
+ syntheticCandidateSnapshot,
897
+ validateCandidate,
898
+ buildPublishedSnapshot,
899
+ fetchOfficialMetadata,
900
+ runPublicationContent,
901
+ };