servicemap 0.1.4 → 0.1.7

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.
package/README.md CHANGED
@@ -41,7 +41,7 @@ npx servicemap skills install --agent claude
41
41
  ```
42
42
 
43
43
  Agents with known local skill folders get the real `servicemap` skill installed.
44
- Other selected AI tools get a portable guide pack under `~/.servicemap/model-guides`.
44
+ Other selected AI tools get setup notes under `~/.servicemap/model-guides`.
45
45
 
46
46
  After a global install, the command is:
47
47
 
@@ -61,6 +61,8 @@ CLI options:
61
61
  servicemap
62
62
  servicemap generate --root <project-path>
63
63
  servicemap --root <project-path>
64
+ servicemap generate --root <project-path> --allow-emails founder@example.com,ops@example.com
65
+ servicemap generate --root <project-path> --allow-email founder@example.com --allow-email ops@example.com
64
66
  servicemap --mode page
65
67
  servicemap --mode desktop
66
68
  servicemap --dry-run
@@ -70,7 +72,7 @@ servicemap install-skill --agent codex
70
72
  servicemap install-skill --agent claude
71
73
  servicemap install-skill --agent grok
72
74
  servicemap install-skill --agent all
73
- servicemap install-skill --targets codex,chatgpt,cursor
75
+ servicemap install-skill --targets codex,gemini,cursor
74
76
  servicemap skills install --agent codex
75
77
  ```
76
78
 
@@ -86,11 +88,11 @@ servicemap skills install --agent codex
86
88
  ## Validation
87
89
 
88
90
  ```bash
89
- node bin/servicemap.mjs generate --root demo-project --dry-run
91
+ node bin/servicemap.mjs generate --root demo-project --allow-emails founder@example.com,ops@example.com --dry-run
90
92
  node bin/servicemap.mjs --root no-website-demo --dry-run
91
93
  node bin/servicemap.mjs --dry-run
92
94
  node bin/servicemap.mjs install-skill --agent all --dry-run
93
- node bin/servicemap.mjs install-skill --targets codex,chatgpt,cursor --dry-run
95
+ node bin/servicemap.mjs install-skill --targets codex,gemini,cursor --dry-run
94
96
  node bin/servicemap.mjs skills install --agent claude --dry-run
95
97
  python3 /Users/dougie/.codex/skills/.system/skill-creator/scripts/quick_validate.py servicemap
96
98
  ```
@@ -13,21 +13,10 @@ const pageScript = resolve(packageRoot, 'servicemap/scripts/generate-page.mjs');
13
13
  const desktopScript = resolve(packageRoot, 'servicemap/scripts/generate-desktop-app.mjs');
14
14
 
15
15
  const targetCatalog = [
16
- { id: 'codex', label: 'Codex / ChatGPT desktop', kind: 'skill', root: () => resolve(homedir(), '.agents/skills'), recommended: true },
17
- { id: 'claude', label: 'Claude Code', kind: 'skill', root: () => resolve(homedir(), '.claude/skills'), recommended: true },
18
- { id: 'grok', label: 'Grok', kind: 'skill', root: () => resolve(homedir(), '.grok/skills'), recommended: true },
19
- { id: 'chatgpt', label: 'ChatGPT', kind: 'guide' },
20
- { id: 'gpt-6-astra', label: 'GPT-6 Astra', kind: 'guide' },
21
- { id: 'gpt-6-sol', label: 'GPT-6 Sol', kind: 'guide' },
22
- { id: 'gpt-6-luna', label: 'GPT-6 Luna', kind: 'guide' },
23
- { id: 'gpt-5.6-sol', label: 'GPT-5.6 Sol', kind: 'guide' },
24
- { id: 'gpt-5.6-terra', label: 'GPT-5.6 Terra', kind: 'guide' },
25
- { id: 'gpt-5.6-luna', label: 'GPT-5.6 Luna', kind: 'guide' },
26
- { id: 'claude-opus', label: 'Claude Opus', kind: 'guide' },
27
- { id: 'claude-sonnet', label: 'Claude Sonnet', kind: 'guide' },
28
- { id: 'claude-haiku', label: 'Claude Haiku', kind: 'guide' },
29
- { id: 'gemini-pro', label: 'Gemini Pro', kind: 'guide' },
30
- { id: 'gemini-flash', label: 'Gemini Flash', kind: 'guide' },
16
+ { id: 'codex', label: 'OpenAI (Codex, ChatGPT, GPT models)', kind: 'skill', root: () => resolve(homedir(), '.agents/skills') },
17
+ { id: 'claude', label: 'Anthropic (Claude Code, Claude models)', kind: 'skill', root: () => resolve(homedir(), '.claude/skills') },
18
+ { id: 'grok', label: 'xAI (Grok)', kind: 'skill', root: () => resolve(homedir(), '.grok/skills') },
19
+ { id: 'gemini', label: 'Google (Gemini, Vertex AI)', kind: 'guide' },
31
20
  { id: 'cursor', label: 'Cursor', kind: 'guide' },
32
21
  { id: 'windsurf', label: 'Windsurf', kind: 'guide' },
33
22
  { id: 'copilot', label: 'GitHub Copilot', kind: 'guide' },
@@ -42,19 +31,14 @@ const targetCatalog = [
42
31
  { id: 'bolt', label: 'Bolt', kind: 'guide' },
43
32
  { id: 'v0', label: 'v0', kind: 'guide' },
44
33
  { id: 'perplexity', label: 'Perplexity', kind: 'guide' },
45
- { id: 'mistral', label: 'Mistral Le Chat', kind: 'guide' },
34
+ { id: 'mistral', label: 'Mistral', kind: 'guide' },
46
35
  { id: 'deepseek', label: 'DeepSeek', kind: 'guide' },
47
36
  { id: 'qwen', label: 'Qwen', kind: 'guide' },
48
- { id: 'llama', label: 'Llama', kind: 'guide' },
49
- { id: 'ollama', label: 'Ollama', kind: 'guide' },
50
- { id: 'lm-studio', label: 'LM Studio', kind: 'guide' },
37
+ { id: 'meta', label: 'Meta (Llama)', kind: 'guide' },
38
+ { id: 'local-models', label: 'Local models (Ollama, LM Studio)', kind: 'guide' },
51
39
  { id: 'openrouter', label: 'OpenRouter', kind: 'guide' },
52
- { id: 'anthropic-api', label: 'Anthropic API apps', kind: 'guide' },
53
- { id: 'openai-api', label: 'OpenAI API apps', kind: 'guide' },
54
- { id: 'gemini-api', label: 'Gemini API apps', kind: 'guide' },
55
40
  { id: 'bedrock', label: 'Amazon Bedrock', kind: 'guide' },
56
41
  { id: 'azure-openai', label: 'Azure OpenAI', kind: 'guide' },
57
- { id: 'vertex-ai', label: 'Vertex AI', kind: 'guide' },
58
42
  { id: 'poe', label: 'Poe', kind: 'guide' },
59
43
  { id: 'you-com', label: 'You.com', kind: 'guide' },
60
44
  { id: 'phind', label: 'Phind', kind: 'guide' },
@@ -65,6 +49,32 @@ const targetCatalog = [
65
49
  { id: 'custom', label: 'Other custom model or agent', kind: 'guide' },
66
50
  ];
67
51
 
52
+ const targetAliases = new Map([
53
+ ['agents', 'codex'],
54
+ ['chatgpt', 'codex'],
55
+ ['openai', 'codex'],
56
+ ['openai-api', 'codex'],
57
+ ['gpt-6-astra', 'codex'],
58
+ ['gpt-6-sol', 'codex'],
59
+ ['gpt-6-luna', 'codex'],
60
+ ['gpt-5.6-sol', 'codex'],
61
+ ['gpt-5.6-terra', 'codex'],
62
+ ['gpt-5.6-luna', 'codex'],
63
+ ['anthropic', 'claude'],
64
+ ['anthropic-api', 'claude'],
65
+ ['claude-opus', 'claude'],
66
+ ['claude-sonnet', 'claude'],
67
+ ['claude-haiku', 'claude'],
68
+ ['google', 'gemini'],
69
+ ['gemini-pro', 'gemini'],
70
+ ['gemini-flash', 'gemini'],
71
+ ['gemini-api', 'gemini'],
72
+ ['vertex-ai', 'gemini'],
73
+ ['llama', 'meta'],
74
+ ['ollama', 'local-models'],
75
+ ['lm-studio', 'local-models'],
76
+ ]);
77
+
68
78
  function parseArgs() {
69
79
  const args = process.argv.slice(2);
70
80
  const options = {
@@ -75,6 +85,7 @@ function parseArgs() {
75
85
  agent: 'codex',
76
86
  targets: null,
77
87
  skillsDir: null,
88
+ allowedEmails: [],
78
89
  invokedWithoutArgs: args.length === 0,
79
90
  explicitCommand: false,
80
91
  generationRequested: false,
@@ -102,6 +113,14 @@ function parseArgs() {
102
113
  }
103
114
  if (arg === '--agent') options.agent = args[++index] ?? options.agent;
104
115
  if (arg === '--targets') options.targets = (args[++index] ?? '').split(',').map((target) => target.trim()).filter(Boolean);
116
+ if (arg === '--allow-email') {
117
+ options.allowedEmails.push(args[++index] ?? '');
118
+ options.generationRequested = true;
119
+ }
120
+ if (arg === '--allow-emails') {
121
+ options.allowedEmails.push(...(args[++index] ?? '').split(','));
122
+ options.generationRequested = true;
123
+ }
105
124
  if (arg === '--skills-dir') options.skillsDir = resolve(args[++index] ?? '.');
106
125
  if (arg === '--user') options.user = true;
107
126
  if (arg === '--survey') options.survey = true;
@@ -119,14 +138,14 @@ function help() {
119
138
 
120
139
  Usage:
121
140
  servicemap
122
- servicemap generate [--root <project>] [--mode auto|page|desktop] [--dry-run]
123
- servicemap --root <project> [--mode auto|page|desktop] [--dry-run]
141
+ servicemap generate [--root <project>] [--mode auto|page|desktop] [--allow-emails <list>] [--dry-run]
142
+ servicemap --root <project> [--mode auto|page|desktop] [--allow-emails <list>] [--dry-run]
124
143
  servicemap install-skill [--survey] [--agent codex|claude|grok|all] [--targets <ids>]
125
144
  servicemap skills install [--survey] [--agent codex|claude|grok|all]
126
145
 
127
146
  Examples:
128
147
  npx servicemap
129
- npx servicemap generate --root ../my-app
148
+ npx servicemap generate --root ../my-app --allow-emails founder@example.com,ops@example.com
130
149
  npx servicemap -- --root ../my-app
131
150
  npx servicemap install-skill
132
151
  npx servicemap install-skill --survey
@@ -160,7 +179,7 @@ async function readScan(root) {
160
179
  function targetsByIds(ids) {
161
180
  const byId = new Map(targetCatalog.map((target) => [target.id, target]));
162
181
  return ids.map((id) => {
163
- const target = byId.get(id);
182
+ const target = byId.get(targetAliases.get(id) ?? id);
164
183
  if (!target) {
165
184
  throw new Error(`Unknown target "${id}". Use --survey to choose from the list.`);
166
185
  }
@@ -171,7 +190,6 @@ function targetsByIds(ids) {
171
190
  function targetsForOptions(options) {
172
191
  if (options.targets?.length) return targetsByIds(options.targets);
173
192
  if (options.agent === 'all') return targetCatalog.filter((target) => target.kind === 'skill');
174
- if (options.agent === 'agents') return targetsByIds(['codex']);
175
193
  return targetsByIds([options.agent]);
176
194
  }
177
195
 
@@ -193,17 +211,16 @@ function renderSurvey(selected, cursor) {
193
211
  targetCatalog.forEach((target, index) => {
194
212
  const pointer = index === cursor ? '>' : ' ';
195
213
  const checked = selected.has(target.id) ? 'x' : ' ';
196
- const detail = target.kind === 'skill' ? 'skill folder' : 'portable guide';
197
- console.log(`${pointer} [${checked}] ${target.label} (${detail})`);
214
+ console.log(`${pointer} [${checked}] ${target.label}`);
198
215
  });
199
216
  }
200
217
 
201
218
  function runTargetSurvey() {
202
219
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
203
- return Promise.resolve(targetCatalog.filter((target) => target.recommended));
220
+ return Promise.resolve([]);
204
221
  }
205
222
 
206
- const selected = new Set(targetCatalog.filter((target) => target.recommended).map((target) => target.id));
223
+ const selected = new Set();
207
224
  let cursor = 0;
208
225
 
209
226
  return new Promise((resolvePromise) => {
@@ -242,7 +259,7 @@ function runTargetSurvey() {
242
259
  function guideText(target) {
243
260
  return `# Servicemap for ${target.label}
244
261
 
245
- Use this instruction with ${target.label} when you want it to create a service map for a software project.
262
+ These setup notes help ${target.label} use Servicemap when you want it to create a service map for a software project.
246
263
 
247
264
  Invoke: /servicemap
248
265
 
@@ -285,11 +302,17 @@ async function installSkill(options) {
285
302
  ? await runTargetSurvey()
286
303
  : targetsForOptions(options);
287
304
  const roots = skillRootsForTargets(options, selectedTargets);
305
+ const notes = guideTargets(selectedTargets);
288
306
 
289
307
  if (!existsSync(source)) {
290
308
  throw new Error(`Bundled skill folder not found: ${source}`);
291
309
  }
292
310
 
311
+ if (roots.length === 0 && notes.length === 0) {
312
+ console.log('No targets selected. Nothing was installed.');
313
+ return;
314
+ }
315
+
293
316
  if (options.dryRun) {
294
317
  for (const root of roots) {
295
318
  console.log(`Would install Servicemap skill to ${join(root, 'servicemap')}`);
@@ -308,7 +331,9 @@ async function installSkill(options) {
308
331
 
309
332
  await writeGuidePacks(selectedTargets, options);
310
333
 
311
- console.log('Restart your AI app if /servicemap does not appear immediately.');
334
+ if (roots.length > 0) {
335
+ console.log('Restart your AI app if /servicemap does not appear immediately.');
336
+ }
312
337
  }
313
338
 
314
339
  async function main() {
@@ -336,7 +361,11 @@ async function main() {
336
361
  throw new Error(`Unknown mode "${options.mode}". Use auto, page, or desktop.`);
337
362
  }
338
363
 
339
- await run(process.execPath, [scanScript, '--root', options.root]);
364
+ const scanArgs = [scanScript, '--root', options.root];
365
+ for (const email of options.allowedEmails) {
366
+ scanArgs.push('--allow-email', email);
367
+ }
368
+ await run(process.execPath, scanArgs);
340
369
  const scan = await readScan(options.root);
341
370
  const selectedMode = options.mode === 'auto'
342
371
  ? scan.project?.framework === 'static' ? 'desktop' : 'page'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "servicemap",
3
- "version": "0.1.4",
3
+ "version": "0.1.7",
4
4
  "description": "Generate an internal service map page or desktop app for a software project.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,7 @@
12
12
  "README.md"
13
13
  ],
14
14
  "scripts": {
15
- "test": "node bin/servicemap.mjs generate --root demo-project --dry-run && node bin/servicemap.mjs --root no-website-demo --dry-run && node bin/servicemap.mjs --dry-run && HOME=\"$(mktemp -d)\" node bin/servicemap.mjs && node bin/servicemap.mjs install-skill --agent all --dry-run && node bin/servicemap.mjs install-skill --targets codex,chatgpt,cursor --dry-run && node bin/servicemap.mjs skills install --agent claude --dry-run",
15
+ "test": "node bin/servicemap.mjs generate --root demo-project --allow-emails founder@example.com,ops@example.com --dry-run && node bin/servicemap.mjs --root no-website-demo --dry-run && node bin/servicemap.mjs --dry-run && HOME=\"$(mktemp -d)\" node bin/servicemap.mjs && node bin/servicemap.mjs install-skill --agent all --dry-run && node bin/servicemap.mjs install-skill --targets codex,gemini,cursor --dry-run && node bin/servicemap.mjs install-skill --targets chatgpt,gpt-6-astra,gemini-flash --dry-run && node bin/servicemap.mjs skills install --agent claude --dry-run",
16
16
  "validate:skill": "python3 /Users/dougie/.codex/skills/.system/skill-creator/scripts/quick_validate.py servicemap"
17
17
  },
18
18
  "keywords": [
@@ -14,13 +14,15 @@ If no website can be found, create a desktop app fallback at `servicemap-desktop
14
14
  ## Workflow
15
15
 
16
16
  1. Inspect the project layout and current web framework.
17
- 2. Run `scripts/scan-project.mjs --root <project-root>` to create `.servicemap/internal-servicemap.json`.
18
- 3. Review the scan output for obvious false positives or missing high-confidence services.
19
- 4. If the scan detects a web framework or routeable website, run `scripts/generate-page.mjs --root <project-root>` to create the framework-specific `/internalservicemap` route.
20
- 5. If the scan reports `framework: static` or no website route can be safely attached, run `scripts/generate-desktop-app.mjs --root <project-root>` to create `servicemap-desktop`.
21
- 6. Adapt the generated surface to the project's existing design system only when the route is clear and the edits are low-risk.
22
- 7. Run the project's relevant build, lint, and focused tests.
23
- 8. Report the page or desktop app path, service count, validation run, and any services marked low or medium confidence.
17
+ 2. If the user gives a list of emails that may access the generated web page, pass them to the scan command with `--allow-email <email>` for each address or `--allow-emails <comma-separated-list>`.
18
+ 3. Run `scripts/scan-project.mjs --root <project-root>` to create `.servicemap/internal-servicemap.json`.
19
+ Include the access flags from the previous step when relevant.
20
+ 4. Review the scan output for obvious false positives or missing high-confidence services.
21
+ 5. If the scan detects a web framework or routeable website, run `scripts/generate-page.mjs --root <project-root>` to create the framework-specific `/internalservicemap` route.
22
+ 6. If the scan reports `framework: static` or no website route can be safely attached, run `scripts/generate-desktop-app.mjs --root <project-root>` to create `servicemap-desktop`.
23
+ 7. Adapt the generated surface to the project's existing design system only when the route is clear and the edits are low-risk.
24
+ 8. Run the project's relevant build, lint, and focused tests.
25
+ 9. Report the page or desktop app path, service count, validation run, any services marked low or medium confidence, and whether requested email access was enforced.
24
26
 
25
27
  ## Safety Rules
26
28
 
@@ -30,6 +32,9 @@ The scan output may include env var names such as `STRIPE_SECRET_KEY`, but not t
30
32
 
31
33
  Do not add authentication, deployment, analytics, or external writes unless the user explicitly asks.
32
34
  If the repository already has route protection conventions for internal pages, follow them.
35
+ When email access is requested, enforce it only through the app's existing server-side authentication.
36
+ The built-in generator can enforce email allowlists for Next.js App Router projects using Clerk by checking `currentUser()` against the configured emails.
37
+ For unsupported frameworks or static output, report that the allowlist is recorded but not enforced.
33
38
  If no website route can be safely attached, create the desktop app fallback instead of forcing website files into an unrelated project.
34
39
 
35
40
  ## Supporting References
@@ -70,6 +70,7 @@ function pageCss() {
70
70
  .service-name span, .evidence { color: #64748b; font-size: 12px; }
71
71
  .badge { display: inline-flex; align-items: center; height: 22px; padding: 0 8px; border-radius: 999px; background: #edf2ff; color: #334155; font-size: 12px; margin-bottom: 10px; }
72
72
  .evidence { margin-top: 8px; }
73
+ .access-warning { margin: 0 0 18px; padding: 12px 14px; border: 1px solid #f5c542; border-radius: 8px; background: #fff7d6; color: #614700; font-size: 14px; line-height: 1.45; }
73
74
  .empty { background: #fff; border: 1px dashed #cbd5e1; border-radius: 8px; padding: 24px; }
74
75
  @media (max-width: 720px) { .servicemap-header { display: block; } .summary { grid-template-columns: 1fr; } }
75
76
  `;
@@ -77,6 +78,11 @@ function pageCss() {
77
78
 
78
79
  function htmlBody(graph) {
79
80
  const services = graph.services ?? [];
81
+ const access = graph.access ?? {};
82
+ const hasAccessRequest = access.requested && access.allowedEmails?.length;
83
+ const accessNotice = hasAccessRequest && access.enforced !== true
84
+ ? '<div class="access-warning">Email access was requested, but this project does not expose a supported server-side auth guard for generated pages. Put this route behind your app auth before sharing it.</div>'
85
+ : '';
80
86
  const serviceCards = services.length
81
87
  ? services.map((service) => {
82
88
  const evidence = service.evidence?.[0];
@@ -111,6 +117,7 @@ function htmlBody(graph) {
111
117
  <div class="metric"><strong>${graph.summary?.highConfidenceCount ?? 0}</strong><span>high confidence</span></div>
112
118
  <div class="metric"><strong>${escapeHtml(graph.project?.framework ?? 'unknown')}</strong><span>framework</span></div>
113
119
  </section>
120
+ ${accessNotice}
114
121
  <section class="service-grid">
115
122
  ${serviceCards}
116
123
  </section>
@@ -136,6 +143,36 @@ ${htmlBody(graph)}
136
143
 
137
144
  function tsxPage(graph) {
138
145
  const html = htmlBody(graph);
146
+ const allowedEmails = graph.access?.allowedEmails ?? [];
147
+ const usesClerk = (graph.services ?? []).some((service) => service.key === 'clerk');
148
+ const canEnforce = allowedEmails.length > 0 && usesClerk && graph.project?.framework === 'next-app-router';
149
+ if (canEnforce) {
150
+ graph.access.enforced = true;
151
+ const guardedHtml = htmlBody(graph);
152
+ return `import { currentUser } from '@clerk/nextjs/server';
153
+ import { notFound } from 'next/navigation';
154
+
155
+ const css = ${JSON.stringify(pageCss())};
156
+ const html = ${JSON.stringify(guardedHtml)};
157
+ const allowedEmails = ${JSON.stringify(allowedEmails)};
158
+
159
+ export default async function InternalServiceMapPage() {
160
+ const user = await currentUser();
161
+ const userEmails = user?.emailAddresses?.map((email) => email.emailAddress.toLowerCase()) ?? [];
162
+ const canView = userEmails.some((email) => allowedEmails.includes(email));
163
+
164
+ if (!canView) notFound();
165
+
166
+ return (
167
+ <>
168
+ <style dangerouslySetInnerHTML={{ __html: css }} />
169
+ <div dangerouslySetInnerHTML={{ __html: html }} />
170
+ </>
171
+ );
172
+ }
173
+ `;
174
+ }
175
+
139
176
  return `const css = ${JSON.stringify(pageCss())};
140
177
  const html = ${JSON.stringify(html)};
141
178
 
@@ -65,16 +65,26 @@ function service(name, key, category, packages, envPrefixes, domains) {
65
65
 
66
66
  function parseArgs() {
67
67
  const args = process.argv.slice(2);
68
- const options = { root: process.cwd(), output: defaultOutput };
68
+ const options = { root: process.cwd(), output: defaultOutput, allowedEmails: [] };
69
69
  for (let index = 0; index < args.length; index += 1) {
70
70
  if (args[index] === '--root') options.root = args[index + 1] ?? options.root;
71
71
  if (args[index] === '--output') options.output = args[index + 1] ?? options.output;
72
+ if (args[index] === '--allow-email') options.allowedEmails.push(args[index + 1] ?? '');
73
+ if (args[index] === '--allow-emails') options.allowedEmails.push(...(args[index + 1] ?? '').split(','));
72
74
  }
73
75
  options.root = resolve(options.root);
74
76
  options.output = resolve(options.root, options.output);
77
+ options.allowedEmails = normalizeEmails(options.allowedEmails);
75
78
  return options;
76
79
  }
77
80
 
81
+ function normalizeEmails(values) {
82
+ return [...new Set(values
83
+ .map((value) => value.trim().toLowerCase())
84
+ .filter(Boolean)
85
+ .filter((value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)))];
86
+ }
87
+
78
88
  async function readJson(path) {
79
89
  try {
80
90
  return JSON.parse(await readFile(path, 'utf8'));
@@ -268,6 +278,10 @@ async function main() {
268
278
  root: options.root,
269
279
  framework: detectFramework(options.root, packageJson, files),
270
280
  },
281
+ access: {
282
+ allowedEmails: options.allowedEmails,
283
+ requested: options.allowedEmails.length > 0,
284
+ },
271
285
  services,
272
286
  edges: buildEdges(projectName, services),
273
287
  summary: {