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
package/tools/cli/i18n.js CHANGED
@@ -48,6 +48,7 @@ const LANGUAGES = {
48
48
  guide_seo: 'SEO / GEO audit (360°)',
49
49
  guide_backup: 'Smart project backup',
50
50
  guide_animated: 'Animated luxury website',
51
+ guide_design: 'Design with Atelier',
51
52
  guide_shield: '\ud83d\udee1\ufe0f GRC Compliance',
52
53
  guide_example_shield_1: '\ud83d\udee1\ufe0f GRC: "Shield, audit my app for GDPR compliance"',
53
54
  guide_example_shield_2: '\ud83d\udee1\ufe0f GRC: "Shield, gap analysis ISO 27001 vs NIST CSF"',
@@ -88,6 +89,7 @@ const LANGUAGES = {
88
89
  guide_example_seo: '🔍 SEO: "/seo audit https://example.com"',
89
90
  guide_example_backup: '🗂️ Backup: "/backup create" → ZIP timestamped',
90
91
  guide_example_animated: '🎬 Animated: "/animated build hero.mp4"',
92
+ guide_example_design: '🎨 Design: "/design read" → Atelier interviews you, then builds',
91
93
  guide_example_osint: '🔍 OSINT: "Shadow, investigate John Doe"',
92
94
  guide_memory: '🧠 Persistent Brain',
93
95
  guide_dev_studio: '🏗️ Dev Studio',
@@ -141,6 +143,7 @@ const LANGUAGES = {
141
143
  guide_seo: 'Audit SEO / GEO (360°)',
142
144
  guide_backup: 'Sauvegarde intelligente',
143
145
  guide_animated: 'Site web animé de luxe',
146
+ guide_design: 'Design avec Atelier',
144
147
  guide_shield: '🛡️ Conformité GRC',
145
148
  guide_example_shield_1: '🛡️ GRC : "Shield, audite mon app pour la conformité RGPD"',
146
149
  guide_example_shield_2: '🛡️ GRC : "Shield, gap analysis ISO 27001 vs NIST CSF"',
@@ -180,6 +183,7 @@ const LANGUAGES = {
180
183
  guide_example_seo: '🔍 SEO : "/seo audit https://example.com"',
181
184
  guide_example_backup: '🗂️ Backup : "/backup create" → ZIP horodaté',
182
185
  guide_example_animated: '🎬 Animé : "/animated build hero.mp4"',
186
+ guide_example_design: '🎨 Design : "/design read" → Atelier vous interroge, puis construit',
183
187
  guide_example_osint: '🔍 OSINT : "Shadow, investigate John Doe"',
184
188
  guide_memory: '🧠 Cerveau persistant',
185
189
  guide_dev_studio: '🏗️ Dev Studio',
@@ -233,6 +237,7 @@ const LANGUAGES = {
233
237
  guide_seo: 'Auditoría SEO / GEO (360°)',
234
238
  guide_backup: 'Copia de seguridad inteligente',
235
239
  guide_animated: 'Sitio web animado de lujo',
240
+ guide_design: 'Diseño con Atelier',
236
241
  guide_shield: '\ud83d\udee1\ufe0f Cumplimiento GRC',
237
242
  guide_example_shield_1: '\ud83d\udee1\ufe0f GRC: "Shield, audita mi app para GDPR"',
238
243
  guide_example_shield_2:
@@ -274,6 +279,7 @@ const LANGUAGES = {
274
279
  guide_example_seo: '🔍 SEO: "/seo audit https://example.com"',
275
280
  guide_example_backup: '🗂️ Backup: "/backup create" → ZIP con marca de tiempo',
276
281
  guide_example_animated: '🎬 Animado: "/animated build hero.mp4"',
282
+ guide_example_design: '🎨 Diseño: "/design read" → Atelier te entrevista y luego construye',
277
283
  guide_example_osint: '🔍 OSINT: "Shadow, investigate John Doe"',
278
284
  guide_memory: '🧠 Cerebro persistente',
279
285
  guide_dev_studio: '🏗️ Dev Studio',
@@ -328,6 +334,7 @@ const LANGUAGES = {
328
334
  guide_seo: 'SEO / GEO Audit (360°)',
329
335
  guide_backup: 'Intelligente Projektsicherung',
330
336
  guide_animated: 'Animierte Luxus-Website',
337
+ guide_design: 'Design mit Atelier',
331
338
  guide_shield: '\ud83d\udee1\ufe0f GRC-Compliance',
332
339
  guide_example_shield_1:
333
340
  '\ud83d\udee1\ufe0f GRC: "Shield, pr\u00fcfe meine App auf DSGVO-Konformit\u00e4t"',
@@ -370,6 +377,7 @@ const LANGUAGES = {
370
377
  guide_example_seo: '🔍 SEO: "/seo audit https://example.com"',
371
378
  guide_example_backup: '🗂️ Backup: "/backup create" → ZIP mit Zeitstempel',
372
379
  guide_example_animated: '🎬 Animiert: "/animated build hero.mp4"',
380
+ guide_example_design: '🎨 Design: "/design read" → Atelier befragt dich, dann baut es',
373
381
  guide_example_osint: '🔍 OSINT: "Shadow, investigate John Doe"',
374
382
  guide_memory: '🧠 Beständiges Gehirn',
375
383
  guide_dev_studio: '🏗️ Dev Studio',
@@ -424,6 +432,7 @@ const LANGUAGES = {
424
432
  guide_seo: 'Auditoria SEO / GEO (360°)',
425
433
  guide_backup: 'Backup inteligente do projeto',
426
434
  guide_animated: 'Site animado de luxo',
435
+ guide_design: 'Design com Atelier',
427
436
  guide_shield: '\ud83d\udee1\ufe0f Conformidade GRC',
428
437
  guide_example_shield_1: '\ud83d\udee1\ufe0f GRC: "Shield, audite meu app para LGPD"',
429
438
  guide_example_shield_2:
@@ -465,6 +474,7 @@ const LANGUAGES = {
465
474
  guide_example_seo: '🔍 SEO: "/seo audit https://example.com"',
466
475
  guide_example_backup: '🗂️ Backup: "/backup create" → ZIP com timestamp',
467
476
  guide_example_animated: '🎬 Animado: "/animated build hero.mp4"',
477
+ guide_example_design: '🎨 Design: "/design read" → Atelier entrevista você, depois constrói',
468
478
  guide_example_osint: '🔍 OSINT: "Shadow, investigate John Doe"',
469
479
  guide_memory: '🧠 Cérebro persistente',
470
480
  guide_dev_studio: '🏗️ Dev Studio',
@@ -519,6 +529,7 @@ const LANGUAGES = {
519
529
  guide_seo: 'SEO / GEO аудит (360°)',
520
530
  guide_backup: 'Умное резервное копирование',
521
531
  guide_animated: 'Анимированный люксовый сайт',
532
+ guide_design: 'Дизайн с Atelier',
522
533
  guide_shield: '🛡️ Соответствие GRC',
523
534
  guide_example_shield_1: '🛡️ GRC: "Shield, проверь моё приложение на GDPR"',
524
535
  guide_example_shield_2: '🛡️ GRC: "Shield, gap-анализ ISO 27001 vs NIST CSF"',
@@ -557,6 +568,7 @@ const LANGUAGES = {
557
568
  guide_example_seo: '🔍 SEO: "/seo audit https://example.com"',
558
569
  guide_example_backup: '🗂️ Бэкап: "/backup create" → ZIP',
559
570
  guide_example_animated: '🎬 Анимация: "/animated build hero.mp4"',
571
+ guide_example_design: '🎨 Дизайн: "/design read" → Atelier опрашивает вас, затем строит',
560
572
  guide_example_osint: '🔍 OSINT: "Shadow, investigate John Doe"',
561
573
  guide_memory: '🧠 Постоянный мозг',
562
574
  guide_dev_studio: '🏗️ Dev Studio',
@@ -610,6 +622,7 @@ const LANGUAGES = {
610
622
  guide_seo: 'SEO / GEO 审计 (360°)',
611
623
  guide_backup: '智能项目备份',
612
624
  guide_animated: '动画奢华网站',
625
+ guide_design: '与 Atelier 一起设计',
613
626
  guide_shield: '🛡️ GRC 合规',
614
627
  guide_example_shield_1: '🛡️ GRC: "Shield, 审查我的应用 GDPR 合规性"',
615
628
  guide_example_shield_2: '🛡️ GRC: "Shield, ISO 27001 vs NIST CSF 差距分析"',
@@ -645,6 +658,7 @@ const LANGUAGES = {
645
658
  guide_example_seo: '🔍 SEO: "/seo audit https://example.com"',
646
659
  guide_example_backup: '🗂️ 备份: "/backup create" → ZIP',
647
660
  guide_example_animated: '🎬 动画: "/animated build hero.mp4"',
661
+ guide_example_design: '🎨 设计: "/design read" → Atelier 先向你提问,再构建',
648
662
  guide_example_osint: '🔍 OSINT: "Shadow, investigate John Doe"',
649
663
  guide_memory: '🧠 持久记忆',
650
664
  guide_dev_studio: '🏗️ Dev Studio',
@@ -698,6 +712,7 @@ const LANGUAGES = {
698
712
  guide_seo: 'ביקורת SEO / GEO (360°)',
699
713
  guide_backup: 'גיבוי חכם של הפרויקט',
700
714
  guide_animated: 'אתר אנימציה יוקרתי',
715
+ guide_design: 'עיצוב עם Atelier',
701
716
  guide_shield: '🛡️ תאימות GRC',
702
717
  guide_example_shield_1: '🛡️ GRC: "Shield, בדוק את האפליקציה שלי ל-GDPR"',
703
718
  guide_example_shield_2: '🛡️ GRC: "Shield, ניתוח פערים ISO 27001 מול NIST CSF"',
@@ -733,6 +748,7 @@ const LANGUAGES = {
733
748
  guide_example_seo: '🔍 SEO: "/seo audit https://example.com"',
734
749
  guide_example_backup: '🗂️ גיבוי: "/backup create" → ZIP',
735
750
  guide_example_animated: '🎬 אנימציה: "/animated build hero.mp4"',
751
+ guide_example_design: '🎨 עיצוב: "/design read" → Atelier מראיין אותך ואז בונה',
736
752
  guide_example_osint: '🔍 OSINT: "Shadow, investigate John Doe"',
737
753
  guide_memory: '🧠 זיכרון מתמשך',
738
754
  guide_dev_studio: '🏗️ Dev Studio',
@@ -786,6 +802,7 @@ const LANGUAGES = {
786
802
  guide_seo: 'SEO / GEO 監査 (360°)',
787
803
  guide_backup: 'スマートプロジェクトバックアップ',
788
804
  guide_animated: 'アニメーション高級ウェブサイト',
805
+ guide_design: 'Atelier でデザイン',
789
806
  guide_shield: '🛡️ GRC コンプライアンス',
790
807
  guide_example_shield_1: '🛡️ GRC: "Shield, アプリのGDPR準拠を監査"',
791
808
  guide_example_shield_2: '🛡️ GRC: "Shield, ISO 27001 vs NIST CSF ギャップ分析"',
@@ -825,6 +842,7 @@ const LANGUAGES = {
825
842
  guide_example_seo: '🔍 SEO: "/seo audit https://example.com"',
826
843
  guide_example_backup: '🗂️ バックアップ: "/backup create" → ZIP',
827
844
  guide_example_animated: '🎬 アニメ: "/animated build hero.mp4"',
845
+ guide_example_design: '🎨 デザイン: "/design read" → Atelier がヒアリングしてから構築します',
828
846
  guide_example_osint: '🔍 OSINT: "Shadow, investigate John Doe"',
829
847
  guide_memory: '🧠 永続的な脳',
830
848
  guide_dev_studio: '🏗️ Dev Studio',
@@ -879,6 +897,7 @@ const LANGUAGES = {
879
897
  guide_seo: 'Audit SEO / GEO (360°)',
880
898
  guide_backup: 'Backup intelligente del progetto',
881
899
  guide_animated: 'Sito web animato di lusso',
900
+ guide_design: 'Design con Atelier',
882
901
  guide_shield: '\ud83d\udee1\ufe0f Conformit\u00e0 GRC',
883
902
  guide_example_shield_1: '\ud83d\udee1\ufe0f GRC: "Shield, verifica la mia app per il GDPR"',
884
903
  guide_example_shield_2: '\ud83d\udee1\ufe0f GRC: "Shield, gap analysis ISO 27001 vs NIST CSF"',
@@ -918,6 +937,7 @@ const LANGUAGES = {
918
937
  guide_example_seo: '🔍 SEO: "/seo audit https://example.com"',
919
938
  guide_example_backup: '🗂️ Backup: "/backup create" → ZIP con timestamp',
920
939
  guide_example_animated: '🎬 Animato: "/animated build hero.mp4"',
940
+ guide_example_design: '🎨 Design: "/design read" → Atelier ti intervista, poi costruisce',
921
941
  guide_example_osint: '🔍 OSINT: "Shadow, investigate John Doe"',
922
942
  guide_memory: '🧠 Memoria persistente',
923
943
  guide_dev_studio: '🏗️ Dev Studio',
@@ -0,0 +1,393 @@
1
+ /**
2
+ * The AI processing register: which AI tools a project uses, what data they see, for what
3
+ * purpose, on which legal basis, for how long and where it goes. `checkRegister` compares it
4
+ * with the integrations present in the project (the adapters BMAD+ installs, the tools'
5
+ * own configuration folders, declared MCP servers) and warns about any it does not cover.
6
+ * The gate is soft: an unregistered tool is a warning, never a failure.
7
+ */
8
+ 'use strict';
9
+
10
+ const fs = require('node:fs');
11
+ const path = require('node:path');
12
+ const yaml = require('js-yaml');
13
+ const { DERIVED } = require('./packs');
14
+
15
+ const SCHEMA = 'bmad-plus/ai-processing-register/1';
16
+ const CHECK_SCHEMA = 'bmad-plus/ai-register-check/1';
17
+ const REGISTER_FILE = '_bmad/ai-processing-register.yaml';
18
+ const MAX_BYTES = 256 * 1024;
19
+ const MAX_TEXT = 2000;
20
+ const REVIEW_INTERVAL_DAYS = 365;
21
+ const TEMPLATE = path.join(
22
+ __dirname,
23
+ '..',
24
+ '..',
25
+ '..',
26
+ 'src',
27
+ 'bmad-plus',
28
+ 'packs',
29
+ 'pack-shield',
30
+ 'shared',
31
+ 'ai-processing-register-template.yaml'
32
+ );
33
+ const BOM = String.fromCharCode(0xfeff);
34
+
35
+ const REGISTER_KEYS = ['schema', 'controller', 'reviewed', 'tools'];
36
+ const ENTRY_KEYS = [
37
+ 'id',
38
+ 'name',
39
+ 'provider',
40
+ 'integrations',
41
+ 'purpose',
42
+ 'data',
43
+ 'personalData',
44
+ 'legalBasis',
45
+ 'retention',
46
+ 'transfers',
47
+ 'agreement',
48
+ ];
49
+ /** GDPR Art. 6(1)(a) to (f). */
50
+ const LEGAL_BASES = [
51
+ 'consent',
52
+ 'contract',
53
+ 'legal-obligation',
54
+ 'vital-interests',
55
+ 'public-task',
56
+ 'legitimate-interests',
57
+ ];
58
+ /** GDPR Art. 45, 46(2)(c), 47 and 49. */
59
+ const TRANSFER_MECHANISMS = [
60
+ 'adequacy-decision',
61
+ 'standard-contractual-clauses',
62
+ 'binding-corporate-rules',
63
+ 'derogation',
64
+ ];
65
+ const ENTRY_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
66
+ /** A tool id, or mcp: and a server name exactly as declared, control characters excepted. */
67
+ const INTEGRATION_ID = /^(?:[a-z0-9][a-z0-9.-]{0,63}|mcp:[^\p{Cc}]{1,256})$/u;
68
+
69
+ /**
70
+ * Where an AI tool shows it is set up in a project, beyond the adapter files the registry
71
+ * declares. Each marker names the integration ids that cover it.
72
+ */
73
+ const TOOL_MARKERS = [
74
+ { ids: ['claude-code'], paths: ['.claude'] },
75
+ { ids: ['cursor'], paths: ['.cursor', '.cursorrules'] },
76
+ { ids: ['gemini-cli', 'antigravity'], paths: ['.gemini'] },
77
+ { ids: ['codex-cli'], paths: ['.codex'] },
78
+ { ids: ['opencode'], paths: ['.opencode', 'opencode.json'] },
79
+ { ids: ['aider'], paths: ['.aider.conf.yml'] },
80
+ { ids: ['github-copilot'], paths: ['.github/copilot-instructions.md'] },
81
+ { ids: ['windsurf'], paths: ['.windsurf', '.windsurfrules'] },
82
+ { ids: ['continue'], paths: ['.continue'] },
83
+ ];
84
+
85
+ /** Project files that declare MCP servers, and the key that holds them. */
86
+ const MCP_FILES = [
87
+ { file: '.mcp.json', key: 'mcpServers' },
88
+ { file: '.cursor/mcp.json', key: 'mcpServers' },
89
+ { file: '.gemini/settings.json', key: 'mcpServers' },
90
+ { file: '.vscode/mcp.json', key: 'servers' },
91
+ ];
92
+
93
+ function fail(message) {
94
+ throw new Error(message);
95
+ }
96
+
97
+ function only(value, keys, where) {
98
+ if (!value || typeof value !== 'object' || Array.isArray(value))
99
+ fail(`${where}: must be a mapping`);
100
+ const unknown = Object.keys(value).filter((key) => !keys.includes(key));
101
+ if (unknown.length) fail(`${where}: unknown key(s) ${unknown.join(', ')}`);
102
+ }
103
+
104
+ function text(value, where) {
105
+ if (typeof value !== 'string' || !value.trim() || value.length > MAX_TEXT)
106
+ fail(`${where}: expected text of 1 to ${MAX_TEXT} characters`);
107
+ return value.trim();
108
+ }
109
+
110
+ function list(value, where, check) {
111
+ if (!Array.isArray(value) || !value.length) fail(`${where}: must be a non-empty list`);
112
+ return value.map((item, index) => check(item, `${where} ${index + 1}`));
113
+ }
114
+
115
+ /** A calendar date written YYYY-MM-DD; YAML may already have parsed it as a Date. */
116
+ function isoDate(value, where) {
117
+ const written = value instanceof Date ? value.toISOString().slice(0, 10) : value;
118
+ if (
119
+ typeof written !== 'string' ||
120
+ !/^\d{4}-\d{2}-\d{2}$/.test(written) ||
121
+ new Date(`${written}T00:00:00Z`).toISOString().slice(0, 10) !== written
122
+ )
123
+ fail(`${where}: expected a date written YYYY-MM-DD`);
124
+ return written;
125
+ }
126
+
127
+ function parseEntry(raw, index) {
128
+ const at = `tool ${raw?.id ?? index + 1}`;
129
+ only(raw, ENTRY_KEYS, at);
130
+ if (typeof raw.id !== 'string' || !ENTRY_ID.test(raw.id)) fail(`${at}: invalid id`);
131
+ if (typeof raw.personalData !== 'boolean')
132
+ fail(`${at}: personalData must say true or false whether the tool sees personal data`);
133
+ if (raw.personalData && !LEGAL_BASES.includes(raw.legalBasis))
134
+ fail(`${at}: legalBasis must be one of ${LEGAL_BASES.join(', ')}`);
135
+ if (!raw.personalData && raw.legalBasis !== undefined)
136
+ fail(`${at}: legalBasis applies only when the tool sees personal data`);
137
+ if (!Array.isArray(raw.transfers))
138
+ fail(`${at}: transfers must list where data goes outside the EEA ([] for nowhere)`);
139
+ return {
140
+ id: raw.id,
141
+ name: text(raw.name, `${at}: name`),
142
+ provider: text(raw.provider, `${at}: provider`),
143
+ integrations: list(raw.integrations, `${at}: integration`, (id, where) => {
144
+ if (typeof id !== 'string' || !INTEGRATION_ID.test(id))
145
+ fail(`${where}: "${id}" is not an integration id (a tool id, or mcp:<server>)`);
146
+ return id;
147
+ }),
148
+ purpose: text(raw.purpose, `${at}: purpose`),
149
+ data: list(raw.data, `${at}: data category`, text),
150
+ personalData: raw.personalData,
151
+ legalBasis: raw.legalBasis ?? null,
152
+ retention: text(raw.retention, `${at}: retention`),
153
+ transfers: raw.transfers.map((transfer, i) => {
154
+ const where = `${at}: transfer ${i + 1}`;
155
+ only(transfer, ['to', 'mechanism'], where);
156
+ if (!TRANSFER_MECHANISMS.includes(transfer.mechanism))
157
+ fail(`${where}: mechanism must be one of ${TRANSFER_MECHANISMS.join(', ')}`);
158
+ return { to: text(transfer.to, `${where}: to`), mechanism: transfer.mechanism };
159
+ }),
160
+ agreement: raw.agreement === undefined ? null : text(raw.agreement, `${at}: agreement`),
161
+ };
162
+ }
163
+
164
+ /** Validate a parsed register. Throws on the first defect. */
165
+ function parseRegister(doc) {
166
+ only(doc, REGISTER_KEYS, 'register');
167
+ if (doc.schema !== SCHEMA) fail(`register: schema must be "${SCHEMA}"`);
168
+ const tools = Array.isArray(doc.tools) ? doc.tools : fail('register: tools must be a list');
169
+ const entries = tools.map(parseEntry);
170
+ const ids = new Set();
171
+ const covered = new Map();
172
+ for (const entry of entries) {
173
+ if (ids.has(entry.id)) fail(`tool ${entry.id}: duplicate id`);
174
+ ids.add(entry.id);
175
+ for (const integration of entry.integrations) {
176
+ if (covered.has(integration))
177
+ fail(`tool ${entry.id}: ${integration} is already covered by ${covered.get(integration)}`);
178
+ covered.set(integration, entry.id);
179
+ }
180
+ }
181
+ return {
182
+ controller: text(doc.controller, 'register: controller'),
183
+ reviewed: isoDate(doc.reviewed, 'register: reviewed'),
184
+ tools: entries,
185
+ };
186
+ }
187
+
188
+ /** A name shortened and with its control characters escaped, safe to print. */
189
+ function printable(name) {
190
+ const shown = name.length > 64 ? `${name.slice(0, 64)}...` : name;
191
+ return shown.replace(/\p{Cc}/gu, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
192
+ }
193
+
194
+ /** The index just past the JSON string that opens at `start`. */
195
+ function stringEnd(text, start) {
196
+ let end = start + 1;
197
+ while (end < text.length && text[end] !== '"') end += text[end] === '\\' ? 2 : 1;
198
+ return end + 1;
199
+ }
200
+
201
+ const CLOSING = /\s*[}\]]/y;
202
+
203
+ /**
204
+ * JSON with comments, as VS Code and Gemini write their settings: line and block comments,
205
+ * then trailing commas, are removed outside strings before the text is parsed as JSON.
206
+ */
207
+ function parseJsonc(source) {
208
+ let bare = '';
209
+ for (let i = 0; i < source.length;) {
210
+ if (source[i] === '"') {
211
+ const end = stringEnd(source, i);
212
+ bare += source.slice(i, end);
213
+ i = end;
214
+ } else if (source.startsWith('//', i)) {
215
+ const end = source.indexOf('\n', i);
216
+ i = end === -1 ? source.length : end;
217
+ } else if (source.startsWith('/*', i)) {
218
+ const end = source.indexOf('*/', i + 2);
219
+ if (end === -1) fail('a block comment is not closed');
220
+ bare += ' ';
221
+ i = end + 2;
222
+ } else bare += source[i++];
223
+ }
224
+ let json = '';
225
+ for (let i = 0; i < bare.length;) {
226
+ if (bare[i] === '"') {
227
+ const end = stringEnd(bare, i);
228
+ json += bare.slice(i, end);
229
+ i = end;
230
+ continue;
231
+ }
232
+ CLOSING.lastIndex = i + 1;
233
+ if (bare[i] !== ',' || !CLOSING.test(bare)) json += bare[i];
234
+ i += 1;
235
+ }
236
+ return JSON.parse(json);
237
+ }
238
+
239
+ /** A regular file or folder inside the project, reached without a symbolic link. */
240
+ function present(root, relative) {
241
+ let current = root;
242
+ for (const part of relative.split('/')) {
243
+ current = path.join(current, part);
244
+ try {
245
+ if (fs.lstatSync(current).isSymbolicLink()) return false;
246
+ } catch {
247
+ return false;
248
+ }
249
+ }
250
+ return true;
251
+ }
252
+
253
+ function readProjectFile(root, relative) {
254
+ if (!present(root, relative)) return null;
255
+ const file = path.join(root, ...relative.split('/'));
256
+ const stat = fs.statSync(file);
257
+ if (!stat.isFile()) return null;
258
+ if (stat.size > MAX_BYTES) fail(`${relative} exceeds ${MAX_BYTES} bytes`);
259
+ const content = fs.readFileSync(file, 'utf8');
260
+ return content.startsWith(BOM) ? content.slice(1) : content;
261
+ }
262
+
263
+ /**
264
+ * The AI integrations present in the project, each with the ids that would cover it and
265
+ * the files that show it. Unreadable MCP declarations come back as warnings.
266
+ */
267
+ function detectIntegrations(projectDir, { adapters = DERIVED.targets.adapters } = {}) {
268
+ const root = path.resolve(projectDir);
269
+ const found = new Map();
270
+ const add = (ids, source) => {
271
+ const key = ids.join('|');
272
+ const entry = found.get(key) || { ids, sources: [] };
273
+ if (!entry.sources.includes(source)) entry.sources.push(source);
274
+ found.set(key, entry);
275
+ };
276
+ // One adapter file can serve several tools (GEMINI.md): any of them covers it.
277
+ const byFile = new Map();
278
+ for (const { tool, file } of adapters) byFile.set(file, [...(byFile.get(file) || []), tool]);
279
+ for (const [file, tools] of byFile) if (present(root, file)) add(tools, file);
280
+ for (const { ids, paths } of TOOL_MARKERS)
281
+ for (const marker of paths) if (present(root, marker)) add(ids, marker);
282
+ const warnings = [];
283
+ for (const { file, key } of MCP_FILES) {
284
+ let doc;
285
+ try {
286
+ const content = readProjectFile(root, file);
287
+ if (content === null) continue;
288
+ doc = parseJsonc(content);
289
+ } catch (error) {
290
+ warnings.push(`${file} cannot be read (${error.message}); its MCP servers are not checked`);
291
+ continue;
292
+ }
293
+ const servers = doc && typeof doc[key] === 'object' && doc[key] ? Object.keys(doc[key]) : [];
294
+ for (const server of servers) {
295
+ // Only a name the register can hold is reported, and never with raw control characters.
296
+ if (INTEGRATION_ID.test(`mcp:${server}`)) add([`mcp:${server}`], file);
297
+ else
298
+ warnings.push(
299
+ `${file} declares the MCP server "${printable(server)}", whose name cannot be registered; it is not checked`
300
+ );
301
+ }
302
+ }
303
+ return { integrations: [...found.values()], warnings };
304
+ }
305
+
306
+ /**
307
+ * Compare the register with the project. Returns the detected integrations with the entry
308
+ * covering each, the register entries matching nothing found here, the warnings, and any
309
+ * error that made the register unreadable.
310
+ */
311
+ function checkRegister(projectDir, { now = new Date(), adapters } = {}) {
312
+ const root = path.resolve(projectDir);
313
+ const detected = detectIntegrations(root, adapters ? { adapters } : {});
314
+ const warnings = [...detected.warnings];
315
+ const errors = [];
316
+ let register = null;
317
+ try {
318
+ const source = readProjectFile(root, REGISTER_FILE);
319
+ if (source !== null) register = parseRegister(yaml.load(source));
320
+ } catch (error) {
321
+ errors.push(`${REGISTER_FILE}: ${error.message}`);
322
+ }
323
+ const covering = new Map();
324
+ for (const entry of register?.tools || [])
325
+ for (const id of entry.integrations) covering.set(id, entry.id);
326
+ const integrations = detected.integrations.map(({ ids, sources }) => ({
327
+ ids,
328
+ sources,
329
+ coveredBy: ids.map((id) => covering.get(id)).find(Boolean) || null,
330
+ }));
331
+ const missing = integrations.filter((item) => !item.coveredBy);
332
+ if (!register && !errors.length && integrations.length)
333
+ warnings.push(`no AI processing register at ${REGISTER_FILE}`);
334
+ for (const item of missing)
335
+ warnings.push(
336
+ `${item.ids.join(' or ')} is set up (${item.sources.join(', ')}) but not in the AI processing register`
337
+ );
338
+ if (register) {
339
+ const age = Math.floor((now - new Date(`${register.reviewed}T00:00:00Z`)) / 86400000);
340
+ if (age > REVIEW_INTERVAL_DAYS)
341
+ warnings.push(`the register was last reviewed on ${register.reviewed}, ${age} days ago`);
342
+ }
343
+ const seen = new Set(integrations.flatMap((item) => item.ids));
344
+ return {
345
+ schema: CHECK_SCHEMA,
346
+ registerFile: REGISTER_FILE,
347
+ register: register ? { controller: register.controller, reviewed: register.reviewed } : null,
348
+ status: errors.length ? 'error' : warnings.length ? 'warning' : 'ok',
349
+ integrations,
350
+ unmatched: (register?.tools || [])
351
+ .filter((entry) => !entry.integrations.some((id) => seen.has(id)))
352
+ .map((entry) => entry.id),
353
+ warnings,
354
+ errors,
355
+ };
356
+ }
357
+
358
+ /**
359
+ * Start the register from the Shield template, never over an existing one. The template's
360
+ * entries are examples: `check` keeps warning until they describe the project's own tools.
361
+ */
362
+ function initRegister(projectDir) {
363
+ const [folder, name] = REGISTER_FILE.split('/');
364
+ const target = path.join(path.resolve(projectDir), folder);
365
+ let stat = null;
366
+ try {
367
+ stat = fs.lstatSync(target);
368
+ } catch (error) {
369
+ if (error.code !== 'ENOENT') throw error;
370
+ }
371
+ // A link in place of the folder would write the register outside the project.
372
+ if (stat && !stat.isDirectory()) fail(`${folder} is not a folder`);
373
+ fs.mkdirSync(target, { recursive: true });
374
+ try {
375
+ fs.writeFileSync(path.join(target, name), fs.readFileSync(TEMPLATE, 'utf8'), { flag: 'wx' });
376
+ } catch (error) {
377
+ if (error.code === 'EEXIST') fail(`${REGISTER_FILE} already exists`);
378
+ throw error;
379
+ }
380
+ return REGISTER_FILE;
381
+ }
382
+
383
+ module.exports = {
384
+ SCHEMA,
385
+ CHECK_SCHEMA,
386
+ REGISTER_FILE,
387
+ LEGAL_BASES,
388
+ TRANSFER_MECHANISMS,
389
+ parseRegister,
390
+ initRegister,
391
+ detectIntegrations,
392
+ checkRegister,
393
+ };