bmad-plus 0.21.0 → 0.23.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 (75) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +24 -14
  3. package/SECURITY.md +63 -1
  4. package/osint-agent-package/skills/bmad-osint-investigate/osint/scripts/_http.py +68 -24
  5. package/package.json +4 -1
  6. package/readme-international/README.de.md +24 -14
  7. package/readme-international/README.es.md +24 -14
  8. package/readme-international/README.fr.md +24 -14
  9. package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +5 -1
  10. package/src/bmad-plus/data/role-triggers.yaml +34 -0
  11. package/src/bmad-plus/module-help.csv +2 -0
  12. package/src/bmad-plus/module.yaml +21 -0
  13. package/src/bmad-plus/packs/pack-design/SKILL.md +72 -0
  14. package/src/bmad-plus/packs/pack-design/bmad-skill-manifest.yaml +13 -0
  15. package/src/bmad-plus/packs/pack-design/design-engine.md +126 -0
  16. package/src/bmad-plus/packs/pack-design/intake.yaml +105 -0
  17. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +3 -1
  18. package/src/bmad-plus/packs/pack-seo/SKILL.md +3 -1
  19. package/src/bmad-plus/packs/pack-seo/ref/cwv-thresholds.md +2 -2
  20. package/src/bmad-plus/packs/pack-seo/requirements.txt +1 -1
  21. package/src/bmad-plus/packs/pack-seo/scripts/seo_apis.py +72 -30
  22. package/src/bmad-plus/packs/pack-seo/scripts/seo_report.py +5 -6
  23. package/src/bmad-plus/packs/pack-seo/scripts/seo_screenshot.py +176 -14
  24. package/src/bmad-plus/packs/pack-shield/README.md +12 -0
  25. package/src/bmad-plus/packs/pack-shield/SKILL.md +7 -1
  26. package/src/bmad-plus/packs/pack-shield/review-rules/access-control.md +10 -0
  27. package/src/bmad-plus/packs/pack-shield/review-rules/ai-integrations.md +10 -0
  28. package/src/bmad-plus/packs/pack-shield/review-rules/change-and-supply-chain.md +10 -0
  29. package/src/bmad-plus/packs/pack-shield/review-rules/cryptography.md +10 -0
  30. package/src/bmad-plus/packs/pack-shield/review-rules/index.yaml +134 -0
  31. package/src/bmad-plus/packs/pack-shield/review-rules/logging.md +10 -0
  32. package/src/bmad-plus/packs/pack-shield/review-rules/personal-data.md +10 -0
  33. package/src/bmad-plus/packs/pack-shield/shared/ai-processing-register-template.yaml +53 -0
  34. package/src/bmad-plus/packs/pack-shield/shared/ai-processing-register.md +32 -0
  35. package/src/bmad-plus/packs/pack-shield/shared/assurance-case-template.yaml +87 -0
  36. package/src/bmad-plus/packs/pack-shield/shared/assurance-case.md +50 -0
  37. package/src/bmad-plus/packs/pack-shield/shield-orchestrator.md +24 -1
  38. package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +7 -0
  39. package/src/bmad-plus/skills/bmad-plus-intake/SKILL.md +87 -0
  40. package/src/bmad-plus/skills/bmad-plus-intake/ref/intake-format.md +112 -0
  41. package/src/bmad-plus/skills/bmad-plus-parallel/SKILL.md +18 -0
  42. package/src/bmad-plus/skills/bmad-plus-swarm/SKILL.md +115 -0
  43. package/src/bmad-plus/skills/bmad-plus-swarm/ref/definition.md +188 -0
  44. package/src/bmad-plus/skills/bmad-plus-uat/SKILL.md +1 -0
  45. package/src/bmad-plus/skills/bmad-plus-uat/template/page.html +5 -4
  46. package/tools/build/adapters.config.js +2 -0
  47. package/tools/build/generate-adapters.js +32 -1
  48. package/tools/build/generate.js +283 -7
  49. package/tools/build/intake-contract.js +522 -0
  50. package/tools/cli/bmad-plus-cli.js +4 -0
  51. package/tools/cli/commands/ai-register.js +63 -0
  52. package/tools/cli/commands/assurance.js +162 -0
  53. package/tools/cli/commands/install.js +28 -1
  54. package/tools/cli/commands/intake.js +124 -0
  55. package/tools/cli/commands/review.js +10 -3
  56. package/tools/cli/commands/swarm.js +119 -0
  57. package/tools/cli/commands/uninstall.js +17 -0
  58. package/tools/cli/i18n.js +20 -0
  59. package/tools/cli/lib/ai-register.js +393 -0
  60. package/tools/cli/lib/assurance.js +822 -0
  61. package/tools/cli/lib/control-refs.js +132 -0
  62. package/tools/cli/lib/installation-health.js +17 -0
  63. package/tools/cli/lib/intake.js +202 -0
  64. package/tools/cli/lib/pack-copy.js +95 -5
  65. package/tools/cli/lib/packs.js +156 -8
  66. package/tools/cli/lib/page-origins.js +582 -0
  67. package/tools/cli/lib/review-rules.js +92 -24
  68. package/tools/cli/lib/review.js +28 -1
  69. package/tools/cli/lib/swarm-deal.js +116 -0
  70. package/tools/cli/lib/swarm-defaults.js +6 -0
  71. package/tools/cli/lib/swarm-tally.js +195 -0
  72. package/tools/cli/lib/swarm-validate.js +589 -0
  73. package/tools/cli/lib/swarm.js +712 -0
  74. package/tools/cli/lib/uat.js +22 -5
  75. package/tools/cli/lib/update-transaction.js +8 -4
@@ -26,6 +26,7 @@ const fs = require('node:fs');
26
26
  const path = require('node:path');
27
27
  const { isDeepStrictEqual } = require('node:util');
28
28
  const yaml = require('js-yaml');
29
+ const { validateIntake } = require('./intake-contract');
29
30
 
30
31
  const REPO_ROOT = path.join(__dirname, '..', '..');
31
32
  const DEFAULT_REGISTRY_PATH = path.join(REPO_ROOT, 'registry.yaml');
@@ -39,6 +40,10 @@ const OWNED_COUNT_RE =
39
40
  /(?<![\w.-])\d+\+?[\s-]+(?:[A-Za-zÀ-ÿ-]+\s+){0,6}(?:agents?|skills?|workflows?|packs?|frameworks?|languages?|reference files)\b/gi;
40
41
  const STANDARD_PREFIX_RE =
41
42
  /\b(?:ISO(?:\/IEC)?|IEC|SOC|NIST(?: SP)?|PCI(?: DSS)?|WCAG|Section)\s*$/i;
43
+ const SKILL_ID_RE = /^[a-z][a-z0-9-]*$/;
44
+ const PACKAGE_NAME_RE = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
45
+ const EXACT_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
46
+ const PIPELINE_GATES = ['require_approval', 'notify_only', 'auto'];
42
47
 
43
48
  function hasOwnedCount(value) {
44
49
  return [...value.matchAll(OWNED_COUNT_RE)].some(
@@ -60,13 +65,16 @@ function assertResourcePath(file, where) {
60
65
  /** Installed entry points, derived from the same layout used by pack-copy.js. */
61
66
  function buildRequiredResources(pack, id = pack.pack_dir) {
62
67
  const where = `registry.yaml: pack "${id}"`;
68
+ const externalSkills = (pack.external_module || {}).skills || [];
63
69
  for (const file of [
64
70
  ...pack.agents,
65
71
  ...(pack.skills || []),
72
+ ...externalSkills,
66
73
  ...(pack.data || []),
67
74
  ...((pack.doctor || {}).pack_agents || []),
68
75
  ...((pack.doctor || {}).required_resources || []),
69
76
  ...(pack.python_entry ? [pack.python_entry] : []),
77
+ ...(pack.intake ? [pack.intake] : []),
70
78
  ]) {
71
79
  assertResourcePath(file, where);
72
80
  }
@@ -79,6 +87,7 @@ function buildRequiredResources(pack, id = pack.pack_dir) {
79
87
  );
80
88
  }
81
89
  resources.push(...(pack.skills || []).map((id) => `.agents/skills/${id}/SKILL.md`));
90
+ resources.push(...externalSkills.map((id) => `.agents/skills/${id}/SKILL.md`));
82
91
  resources.push(...(pack.data || []).map((file) => `.agents/data/${file}`));
83
92
  resources.push(
84
93
  ...((pack.doctor || {}).required_resources || []).map(
@@ -86,10 +95,137 @@ function buildRequiredResources(pack, id = pack.pack_dir) {
86
95
  )
87
96
  );
88
97
  if (pack.python_entry) resources.push(`.agents/skills/${pack.pack_dir}/${pack.python_entry}`);
98
+ if (pack.intake) resources.push(`.agents/skills/${pack.pack_dir}/${pack.intake}`);
89
99
  for (const file of resources) assertResourcePath(file, where);
90
100
  return [...new Set(resources)];
91
101
  }
92
102
 
103
+ function assertSkillIds(list, where) {
104
+ if (
105
+ !Array.isArray(list) ||
106
+ list.length === 0 ||
107
+ list.some((id) => typeof id !== 'string' || !SKILL_ID_RE.test(id)) ||
108
+ new Set(list).size !== list.length
109
+ ) {
110
+ throw new Error(`${where}: must be a non-empty list of distinct skill ids`);
111
+ }
112
+ }
113
+
114
+ /** Structural contract of `external_module` (§5.2); the package itself is checked by --check. */
115
+ function validateExternalModule(pack, where) {
116
+ const module = pack.external_module;
117
+ if (module === undefined) return;
118
+ if (
119
+ !module ||
120
+ typeof module !== 'object' ||
121
+ Object.keys(module).some((key) => !['package', 'version', 'skills'].includes(key))
122
+ ) {
123
+ throw new Error(`${where}: external_module takes exactly package, version and skills`);
124
+ }
125
+ if (typeof module.package !== 'string' || !PACKAGE_NAME_RE.test(module.package)) {
126
+ throw new Error(`${where}: external_module.package must be an npm package name`);
127
+ }
128
+ if (typeof module.version !== 'string' || !EXACT_VERSION_RE.test(module.version)) {
129
+ throw new Error(`${where}: external_module.version must be an exact version`);
130
+ }
131
+ assertSkillIds(module.skills, `${where}: external_module.skills`);
132
+ for (const id of module.skills) {
133
+ if ((pack.skills || []).includes(id)) {
134
+ throw new Error(`${where}: skill ${id} is both shipped in-tree and taken from the module`);
135
+ }
136
+ }
137
+ }
138
+
139
+ /** Structural contract of `pipelines` (§5.3): every step names one real thing of the pack. */
140
+ function validatePipelines(pack, where) {
141
+ if (pack.pipelines === undefined) return;
142
+ if (!Array.isArray(pack.pipelines) || pack.pipelines.length === 0) {
143
+ throw new Error(`${where}: pipelines must be a non-empty list`);
144
+ }
145
+ const workflows = new Set([
146
+ ...(pack.workflows || []),
147
+ ...(pack.categories || []).flatMap((category) => category.workflows || []),
148
+ ]);
149
+ const agents = new Set([...pack.agents, ...(pack.sub_agents || [])]);
150
+ const ids = new Set();
151
+ for (const pipeline of pack.pipelines) {
152
+ if (
153
+ !pipeline ||
154
+ typeof pipeline !== 'object' ||
155
+ Object.keys(pipeline).some((key) => !['id', 'title', 'steps'].includes(key))
156
+ ) {
157
+ throw new Error(`${where}: each pipeline takes exactly id, title and steps`);
158
+ }
159
+ if (typeof pipeline.id !== 'string' || !SKILL_ID_RE.test(pipeline.id) || ids.has(pipeline.id)) {
160
+ throw new Error(`${where}: pipeline ids must be distinct lowercase slugs`);
161
+ }
162
+ ids.add(pipeline.id);
163
+ const label = `${where}: pipeline ${pipeline.id}`;
164
+ if (typeof pipeline.title !== 'string' || !pipeline.title.trim()) {
165
+ throw new Error(`${label}: needs a title`);
166
+ }
167
+ if (!Array.isArray(pipeline.steps) || pipeline.steps.length === 0) {
168
+ throw new Error(`${label}: needs at least one step`);
169
+ }
170
+ pipeline.steps.forEach((step, index) => {
171
+ const at = `${label}: step ${index + 1}`;
172
+ if (
173
+ !step ||
174
+ typeof step !== 'object' ||
175
+ Object.keys(step).some((key) => !['workflow', 'agent', 'gate'].includes(key))
176
+ ) {
177
+ throw new Error(`${at}: takes workflow or agent, and gate`);
178
+ }
179
+ if ((step.workflow === undefined) === (step.agent === undefined)) {
180
+ throw new Error(`${at}: names exactly one of workflow or agent`);
181
+ }
182
+ if (step.workflow !== undefined && !workflows.has(step.workflow)) {
183
+ throw new Error(`${at}: workflow ${step.workflow} is not declared by the pack`);
184
+ }
185
+ if (step.agent !== undefined && !agents.has(step.agent)) {
186
+ throw new Error(`${at}: agent ${step.agent} is not declared by the pack`);
187
+ }
188
+ if (!PIPELINE_GATES.includes(step.gate)) {
189
+ throw new Error(`${at}: gate must be one of ${PIPELINE_GATES.join('|')}`);
190
+ }
191
+ });
192
+ }
193
+ }
194
+
195
+ /** `intake` names a file inside the pack directory (§5.4); its content is checked by --check. */
196
+ function validateIntakeField(pack, where) {
197
+ if (pack.intake === undefined) return;
198
+ if (typeof pack.intake !== 'string' || !pack.intake) {
199
+ throw new Error(`${where}: intake must be a file path relative to pack_dir`);
200
+ }
201
+ if (!pack.pack_dir) throw new Error(`${where}: intake needs a pack_dir to live in`);
202
+ }
203
+
204
+ /** Every `uses_skills` id (§5.1) is a skill some pack ships in-tree or through its module. */
205
+ function validateUsesSkills(registry) {
206
+ const known = new Set();
207
+ for (const pack of Object.values(registry.packs)) {
208
+ for (const id of pack.skills || []) known.add(id);
209
+ for (const id of (pack.external_module || {}).skills || []) known.add(id);
210
+ }
211
+ const resolve = (list, where) => {
212
+ if (list === undefined) return;
213
+ assertSkillIds(list, where);
214
+ for (const id of list) {
215
+ if (!known.has(id)) throw new Error(`${where}: skill ${id} is shipped by no pack`);
216
+ }
217
+ };
218
+ for (const [id, pack] of Object.entries(registry.packs)) {
219
+ resolve(pack.uses_skills, `registry.yaml: pack "${id}": uses_skills`);
220
+ for (const persona of pack.personas || []) {
221
+ resolve(
222
+ persona.uses_skills,
223
+ `registry.yaml: pack "${id}": persona ${persona.id}: uses_skills`
224
+ );
225
+ }
226
+ }
227
+ }
228
+
93
229
  /**
94
230
  * Validate the minimal contract the generator relies on.
95
231
  * Throws with a precise message on the first violation.
@@ -160,6 +296,15 @@ function validateRegistry(registry) {
160
296
  ) {
161
297
  throw new Error(`${where}: runtime must declare known runtimes`);
162
298
  }
299
+ validateExternalModule(pack, where);
300
+ validatePipelines(pack, where);
301
+ validateIntakeField(pack, where);
302
+ if (
303
+ pack.cohabitation_warning !== undefined &&
304
+ (typeof pack.cohabitation_warning !== 'string' || !pack.cohabitation_warning.trim())
305
+ ) {
306
+ throw new Error(`${where}: cohabitation_warning must be a non-empty sentence`);
307
+ }
163
308
  buildRequiredResources(pack, id);
164
309
  if (pack.personas !== undefined) {
165
310
  if (!Array.isArray(pack.personas)) throw new Error(`${where}: personas must be a list`);
@@ -183,6 +328,7 @@ function validateRegistry(registry) {
183
328
  }
184
329
  }
185
330
  }
331
+ validateUsesSkills(registry);
186
332
  const integration = registry.targets?.integration;
187
333
  if (
188
334
  integration?.execution !== 'host-managed' ||
@@ -242,6 +388,21 @@ function buildPackOrder(registry) {
242
388
  return sortedPackEntries(registry).map(([id]) => id);
243
389
  }
244
390
 
391
+ function externalModuleFacts({ package: name, version, skills }) {
392
+ return { package: name, version, skills: [...skills] };
393
+ }
394
+
395
+ function pipelineFacts(pipelines) {
396
+ return pipelines.map(({ id, title, steps }) => ({
397
+ id,
398
+ title,
399
+ steps: steps.map((step) => ({
400
+ ...(step.workflow !== undefined ? { workflow: step.workflow } : { agent: step.agent }),
401
+ gate: step.gate,
402
+ })),
403
+ }));
404
+ }
405
+
245
406
  /**
246
407
  * Generate the PACKS object with the exact shape of tools/cli/lib/packs.js.
247
408
  * Key-presence rules mirror the hand-written module:
@@ -262,10 +423,14 @@ function buildPacks(registry) {
262
423
  };
263
424
  if ('data' in p) entry.data = [...p.data];
264
425
  if (p.external_package) entry.externalPackage = p.external_package;
426
+ if (p.external_module) entry.externalModule = externalModuleFacts(p.external_module);
265
427
  if (p.pack_dir) {
266
428
  entry.packDir = p.pack_dir;
267
429
  entry.packSrcDir = p.pack_src_dir || 'packs';
268
430
  }
431
+ if (p.uses_skills) entry.usesSkills = [...p.uses_skills];
432
+ if (p.pipelines) entry.pipelines = pipelineFacts(p.pipelines);
433
+ if (p.intake) entry.intake = p.intake;
269
434
  if (p.required === true) entry.required = true;
270
435
  entry.desc = derived.packs[id].desc;
271
436
  packs[id] = entry;
@@ -302,6 +467,19 @@ function renderSummary(pack, derived, field = 'summary') {
302
467
  });
303
468
  }
304
469
 
470
+ /**
471
+ * The package flows of the data-handling doctrine that can leave the machine, with their
472
+ * consent basis; maintainer services are not installed and stay out. verify-egress.js owns
473
+ * the doctrine's schema and its agreement with the code.
474
+ */
475
+ function buildDataHandling(registry) {
476
+ const doctrine = registry.data_handling || {};
477
+ const services = new Set((doctrine.services || []).map((service) => service.id));
478
+ return (doctrine.flows || [])
479
+ .filter((flow) => flow.egress !== 'none' && !services.has(flow.component))
480
+ .map((flow) => ({ id: flow.id, component: flow.component, consent: flow.consent?.basis }));
481
+ }
482
+
305
483
  /** The one derivation engine; its JSON result is shipped for runtime consumers. */
306
484
  function buildDerived(registry, { sourceRoot = path.join(REPO_ROOT, 'src', 'bmad-plus') } = {}) {
307
485
  const packs = {};
@@ -339,13 +517,23 @@ function buildDerived(registry, { sourceRoot = path.join(REPO_ROOT, 'src', 'bmad
339
517
  categoryAgentCounts,
340
518
  runtimes: [...pack.runtime],
341
519
  requiredResources: buildRequiredResources(pack, id),
342
- personas: (pack.personas || []).map(({ id, name, role, description, alias }) => ({
343
- id,
344
- name,
345
- role,
346
- description,
347
- ...(alias ? { alias } : {}),
348
- })),
520
+ personas: (pack.personas || []).map(
521
+ ({ id, name, role, description, alias, uses_skills }) => ({
522
+ id,
523
+ name,
524
+ role,
525
+ description,
526
+ ...(alias ? { alias } : {}),
527
+ ...(uses_skills ? { usesSkills: [...uses_skills] } : {}),
528
+ })
529
+ ),
530
+ ...(pack.uses_skills ? { usesSkills: [...pack.uses_skills] } : {}),
531
+ ...(pack.external_module
532
+ ? { externalModule: externalModuleFacts(pack.external_module) }
533
+ : {}),
534
+ ...(pack.pipelines ? { pipelines: pipelineFacts(pack.pipelines) } : {}),
535
+ ...(pack.intake ? { intake: pack.intake } : {}),
536
+ ...(pack.cohabitation_warning ? { cohabitationWarning: pack.cohabitation_warning } : {}),
349
537
  };
350
538
  facts.summary = renderSummary(pack, facts);
351
539
  facts.desc = renderSummary(pack, facts, 'description');
@@ -381,6 +569,7 @@ function buildDerived(registry, { sourceRoot = path.join(REPO_ROOT, 'src', 'bmad
381
569
  languages: Object.keys(LANGUAGES),
382
570
  packs,
383
571
  pythonPacks,
572
+ dataHandling: buildDataHandling(registry),
384
573
  diagnostics: {
385
574
  schemaVersion: 1,
386
575
  processExecution: registry.targets.optional_process_backend || null,
@@ -562,6 +751,88 @@ function checkSourcePaths(packs, packageRoot = REPO_ROOT) {
562
751
  return missing;
563
752
  }
564
753
 
754
+ function resolvePackageManifest(name, packageRoot) {
755
+ return require.resolve(name + '/package.json', { paths: [packageRoot] });
756
+ }
757
+
758
+ /**
759
+ * Every `external_module` is an exact pinned dependency, resolvable from the
760
+ * repository root at the declared version, and ships each listed skill.
761
+ */
762
+ function checkExternalModules(
763
+ registry,
764
+ { packageRoot = REPO_ROOT, packageJson, resolve = resolvePackageManifest } = {}
765
+ ) {
766
+ const problems = [];
767
+ const dependencies = (packageJson && packageJson.dependencies) || {};
768
+ const declared = new Set((registry.data_handling || {}).runtime_dependencies || []);
769
+ for (const [id, pack] of Object.entries(registry.packs)) {
770
+ const module = pack.external_module;
771
+ if (!module) continue;
772
+ const where = `PACKS.${id}: external_module ${module.package}`;
773
+ if (dependencies[module.package] !== module.version) {
774
+ problems.push(
775
+ `${where}: package.json must pin dependencies["${module.package}"] to exactly "${module.version}" (found ${JSON.stringify(dependencies[module.package])})`
776
+ );
777
+ }
778
+ if (!declared.has(module.package)) {
779
+ problems.push(`${where}: missing from data_handling.runtime_dependencies`);
780
+ }
781
+ let manifestPath;
782
+ try {
783
+ manifestPath = resolve(module.package, packageRoot);
784
+ } catch (error) {
785
+ problems.push(`${where}: cannot be resolved from the repository root (${error.message})`);
786
+ continue;
787
+ }
788
+ let manifest;
789
+ try {
790
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
791
+ } catch (error) {
792
+ problems.push(`${where}: unreadable package.json (${error.message})`);
793
+ continue;
794
+ }
795
+ if (!manifest || manifest.name !== module.package || manifest.version !== module.version) {
796
+ problems.push(
797
+ `${where}: installed ${manifest && manifest.name}@${manifest && manifest.version}, declared ${module.version}`
798
+ );
799
+ }
800
+ for (const skill of module.skills) {
801
+ const file = path.join(path.dirname(manifestPath), 'skills', skill, 'SKILL.md');
802
+ if (!fs.existsSync(file)) problems.push(`${where}: skills/${skill}/SKILL.md is missing`);
803
+ }
804
+ }
805
+ return problems;
806
+ }
807
+
808
+ /** Every declared intake file exists in its pack directory and passes the contract. */
809
+ function checkIntakes(registry, { packageRoot = REPO_ROOT } = {}) {
810
+ const problems = [];
811
+ for (const [id, pack] of Object.entries(registry.packs)) {
812
+ if (!pack.intake) continue;
813
+ const relative = `src/bmad-plus/${pack.pack_src_dir || 'packs'}/${pack.pack_dir}/${pack.intake}`;
814
+ const file = path.join(packageRoot, ...relative.split('/'));
815
+ const where = `PACKS.${id}: intake ${relative}`;
816
+ if (!fs.existsSync(file)) {
817
+ problems.push(`${where}: file does not exist`);
818
+ continue;
819
+ }
820
+ let parsed;
821
+ try {
822
+ parsed = yaml.load(fs.readFileSync(file, 'utf8'), { schema: yaml.JSON_SCHEMA });
823
+ } catch (error) {
824
+ problems.push(`${where}: ${error.message}`);
825
+ continue;
826
+ }
827
+ const result = validateIntake(parsed);
828
+ for (const problem of result.problems) problems.push(`${where}: ${problem}`);
829
+ if (result.ok && parsed.pack !== id) {
830
+ problems.push(`${where}: declares pack "${parsed.pack}" instead of "${id}"`);
831
+ }
832
+ }
833
+ return problems;
834
+ }
835
+
565
836
  /**
566
837
  * `--check` mode: prove registry.yaml reproduces the live packs.js exactly,
567
838
  * and that product.version matches package.json.
@@ -581,6 +852,8 @@ function check({
581
852
 
582
853
  const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
583
854
  mismatches.push(...checkSourcePaths(generated.PACKS));
855
+ mismatches.push(...checkExternalModules(registry, { packageJson: pkg }));
856
+ mismatches.push(...checkIntakes(registry));
584
857
  const { checkPythonDelivery } = require('./pack-delivery');
585
858
  mismatches.push(...checkPythonDelivery(registry, { packageRoot: REPO_ROOT, packageJson: pkg }));
586
859
  try {
@@ -666,6 +939,7 @@ module.exports = {
666
939
  DEFAULT_PACKS_MODULE_PATH,
667
940
  DEFAULT_MODULE_PATH,
668
941
  MODULE_TEMPLATE_PATH,
942
+ PACKAGE_NAME_RE,
669
943
  loadRegistry,
670
944
  validateRegistry,
671
945
  buildPacks,
@@ -678,6 +952,8 @@ module.exports = {
678
952
  generatePacksModuleSource,
679
953
  check,
680
954
  checkSourcePaths,
955
+ checkExternalModules,
956
+ checkIntakes,
681
957
  main,
682
958
  };
683
959