@capitalthought/agentsfirst-mcp 0.1.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.
package/dist/score.js ADDED
@@ -0,0 +1,614 @@
1
+ // Scoring — pure functions over probe signals.
2
+ //
3
+ // Implements the rubric from ~/.claude/skills/agentsfirst/SKILL.md.
4
+ // 100 points total. Maps to a 0–4 adoption level.
5
+ import { PRINCIPLE_SLUGS, SMALLEST_EXPERIMENT, } from './principles.js';
6
+ const LEVEL_NAMES = {
7
+ 0: 'No agent access',
8
+ 1: 'Agent as Afterthought',
9
+ 2: 'Agent-Aware',
10
+ 3: 'Agents First',
11
+ 4: 'Agent-Driven',
12
+ };
13
+ function levelFor(score) {
14
+ if (score <= 10)
15
+ return 0;
16
+ if (score <= 25)
17
+ return 1;
18
+ if (score <= 60)
19
+ return 2;
20
+ if (score <= 85)
21
+ return 3;
22
+ return 4;
23
+ }
24
+ function statusFor(pts, max) {
25
+ if (pts === 0)
26
+ return 'fail';
27
+ if (pts >= max * 0.75)
28
+ return 'pass';
29
+ return 'partial';
30
+ }
31
+ // ─── Codebase scoring ─────────────────────────────────────────────────────────
32
+ export function scoreCodebase(signals) {
33
+ const principles = {};
34
+ // Interface First (20)
35
+ principles['interface-first'] = scoreInterfaceFirst(signals);
36
+ // Contract First (15)
37
+ principles['contract-first'] = scoreContractFirst(signals);
38
+ // Prep Gates (10)
39
+ principles['prep-gates'] = scorePrepGates(signals);
40
+ // Typed State (10)
41
+ principles['typed-state'] = scoreTypedState(signals);
42
+ // Visible Outputs (10)
43
+ principles['visible-outputs'] = scoreVisibleOutputs(signals);
44
+ // Multi-Model Verification (5)
45
+ principles['multi-model-verification'] = scoreMultiModel(signals);
46
+ // Perspective Dispatch (5)
47
+ principles['perspective-dispatch'] = scorePerspectiveDispatch(signals);
48
+ // Autonomous Recovery (10)
49
+ principles['autonomous-recovery'] = scoreRecovery(signals);
50
+ // Discoverability bonus (15)
51
+ principles['discoverability'] = scoreDiscoverability(signals);
52
+ const score = Object.values(principles).reduce((a, b) => a + b.pts, 0);
53
+ const level = levelFor(score);
54
+ const anti_patterns_flagged = flagAntiPatterns(signals);
55
+ const top_moves = computeTopMoves(principles);
56
+ return {
57
+ score,
58
+ level,
59
+ level_name: LEVEL_NAMES[level],
60
+ principles,
61
+ anti_patterns_flagged,
62
+ top_moves,
63
+ probe_signals: signals,
64
+ };
65
+ }
66
+ function scoreInterfaceFirst(signals) {
67
+ const max = 20;
68
+ let pts = 0;
69
+ const notes = [];
70
+ const mcp = signals.signals.mcp_server;
71
+ const cli = signals.signals.cli;
72
+ const sdk = signals.signals.typed_sdk;
73
+ if (mcp.detected) {
74
+ pts += 10;
75
+ notes.push('MCP server detected');
76
+ }
77
+ else if (cli.detected) {
78
+ pts += 8;
79
+ notes.push('CLI detected (no MCP server)');
80
+ }
81
+ else if (sdk.detected) {
82
+ pts += 6;
83
+ notes.push('Typed SDK detected (no MCP, no CLI)');
84
+ }
85
+ else {
86
+ notes.push('No agent interface found — no MCP, no CLI, no typed SDK');
87
+ }
88
+ // Verb-first tool names (5)
89
+ const verbRatio = mcp.indicators.verb_first_ratio ?? 0;
90
+ if (verbRatio >= 0.7) {
91
+ pts += 5;
92
+ notes.push(`Verb-first tool names (ratio ${(verbRatio * 100).toFixed(0)}%)`);
93
+ }
94
+ else if (verbRatio > 0) {
95
+ pts += 2;
96
+ notes.push(`Mixed naming — verb-first ratio only ${(verbRatio * 100).toFixed(0)}%`);
97
+ }
98
+ // Typed parameters (5)
99
+ if (mcp.indicators.uses_zod_for_params) {
100
+ pts += 5;
101
+ notes.push('Zod-typed parameters');
102
+ }
103
+ else if (signals.signals.typed_state.indicators.zod_imports > 0) {
104
+ pts += 2;
105
+ notes.push('Zod present but not clearly bound to tool params');
106
+ }
107
+ return { pts: Math.min(pts, max), max, status: statusFor(pts, max), notes };
108
+ }
109
+ function scoreContractFirst(signals) {
110
+ const max = 15;
111
+ let pts = 0;
112
+ const notes = [];
113
+ const md = signals.signals.agents_md;
114
+ if (md.exists) {
115
+ pts += 10;
116
+ notes.push(`AGENTS.md found at ${md.path}`);
117
+ const sections = md.sections;
118
+ if (sections) {
119
+ const covered = ['permissions', 'sequence', 'identifiers', 'errors'].filter((k) => sections[k]).length;
120
+ const sectionPts = Math.round((covered / 4) * 5);
121
+ pts += sectionPts;
122
+ notes.push(`${covered}/4 required sections (permissions/sequence/identifiers/errors) → +${sectionPts}`);
123
+ }
124
+ }
125
+ else {
126
+ notes.push('No AGENTS.md');
127
+ }
128
+ return { pts: Math.min(pts, max), max, status: statusFor(pts, max), notes };
129
+ }
130
+ function scorePrepGates(signals) {
131
+ const max = 10;
132
+ let pts = 0;
133
+ const notes = [];
134
+ const pg = signals.signals.prep_gate;
135
+ if (pg.detected) {
136
+ pts += 10;
137
+ if (pg.indicators.prep_files?.length) {
138
+ notes.push(`Prep file(s) detected: ${pg.indicators.prep_files.length}`);
139
+ }
140
+ if (pg.indicators.prep_tool_registrations) {
141
+ notes.push(`<project>_prep tool registrations: ${pg.indicators.prep_tool_registrations}`);
142
+ }
143
+ if (pg.indicators.prep_function_exported) {
144
+ notes.push('Exported runPrep()/preflightCheck()');
145
+ }
146
+ }
147
+ else {
148
+ notes.push('No prep gate — agents will start sessions on stale state');
149
+ }
150
+ return { pts, max, status: statusFor(pts, max), notes };
151
+ }
152
+ function scoreTypedState(signals) {
153
+ const max = 10;
154
+ let pts = 0;
155
+ const notes = [];
156
+ const ts = signals.signals.typed_state;
157
+ if (ts.detected) {
158
+ pts += 5;
159
+ const which = [];
160
+ if (ts.indicators.zod_imports > 0)
161
+ which.push('zod');
162
+ if (ts.indicators.json_schema_files > 0)
163
+ which.push('json-schema');
164
+ if (ts.indicators.prisma)
165
+ which.push('prisma');
166
+ if (ts.indicators.drizzle > 0)
167
+ which.push('drizzle');
168
+ notes.push(`Typed schema layer present (${which.join(', ') || 'detected'})`);
169
+ }
170
+ else {
171
+ notes.push('No structured-schema layer found');
172
+ }
173
+ if (ts.has_migrations) {
174
+ pts += 5;
175
+ notes.push(`Versioned migrations (${ts.indicators.migration_files} files / prisma=${ts.indicators.prisma})`);
176
+ }
177
+ else {
178
+ notes.push('No migration files detected');
179
+ }
180
+ return { pts, max, status: statusFor(pts, max), notes };
181
+ }
182
+ function scoreVisibleOutputs(signals) {
183
+ const max = 10;
184
+ let pts = 0;
185
+ const notes = [];
186
+ const vo = signals.signals.visible_outputs;
187
+ if (vo.detected) {
188
+ pts += 10;
189
+ const sinks = [];
190
+ if (vo.indicators.slack_imports > 0)
191
+ sinks.push('slack');
192
+ if (vo.indicators.postmark > 0)
193
+ sinks.push('postmark');
194
+ if (vo.indicators.resend > 0)
195
+ sinks.push('resend');
196
+ if (vo.indicators.email_general > 0)
197
+ sinks.push('email');
198
+ if (vo.indicators.linear > 0)
199
+ sinks.push('linear');
200
+ if (vo.indicators.audit_log_files > 0)
201
+ sinks.push('audit-log');
202
+ notes.push(`Visible-output sinks: ${sinks.join(', ')}`);
203
+ }
204
+ else if (vo.indicators.todo_ship_markers > 0) {
205
+ pts += 3;
206
+ notes.push(`${vo.indicators.todo_ship_markers} TODO ship-to markers — outputs planned but not wired`);
207
+ }
208
+ else {
209
+ notes.push('No human-visible output sink detected');
210
+ }
211
+ return { pts, max, status: statusFor(pts, max), notes };
212
+ }
213
+ function scoreMultiModel(signals) {
214
+ const max = 5;
215
+ let pts = 0;
216
+ const notes = [];
217
+ const mm = signals.signals.multi_model;
218
+ if (mm.detected) {
219
+ pts += 5;
220
+ notes.push(`Multi-model verification wired (${mm.indicators.distinct_llm_sdks.join(' + ')})`);
221
+ }
222
+ else if (mm.indicators.distinct_llm_sdks.length >= 2) {
223
+ pts += 2;
224
+ notes.push(`${mm.indicators.distinct_llm_sdks.length} LLM SDKs present but no consensus pattern`);
225
+ }
226
+ else if (mm.indicators.distinct_llm_sdks.length === 1) {
227
+ notes.push(`Single LLM SDK only (${mm.indicators.distinct_llm_sdks[0]})`);
228
+ }
229
+ else {
230
+ notes.push('No LLM SDKs in dependency tree');
231
+ }
232
+ return { pts, max, status: statusFor(pts, max), notes };
233
+ }
234
+ function scorePerspectiveDispatch(signals) {
235
+ const max = 5;
236
+ let pts = 0;
237
+ const notes = [];
238
+ const pd = signals.signals.perspective_dispatch;
239
+ if (pd.detected) {
240
+ pts += 5;
241
+ notes.push(`Perspective dispatch detected (personas=${pd.indicators.persona_files ?? 0}, reviewer hits=${pd.indicators.reviewer_pattern})`);
242
+ }
243
+ else {
244
+ notes.push('No multi-perspective reviewer pattern');
245
+ }
246
+ return { pts, max, status: statusFor(pts, max), notes };
247
+ }
248
+ function scoreRecovery(signals) {
249
+ const max = 10;
250
+ let pts = 0;
251
+ const notes = [];
252
+ const rec = signals.signals.autonomous_recovery;
253
+ if (rec.detected) {
254
+ pts += 5;
255
+ notes.push('Retry-with-backoff helper present');
256
+ }
257
+ else {
258
+ notes.push('No retry helper');
259
+ }
260
+ if (rec.has_escalation) {
261
+ pts += 5;
262
+ notes.push('Structured escalation pattern present');
263
+ }
264
+ else {
265
+ notes.push('No structured escalation found');
266
+ }
267
+ return { pts, max, status: statusFor(pts, max), notes };
268
+ }
269
+ function scoreDiscoverability(signals) {
270
+ const max = 15;
271
+ let pts = 0;
272
+ const notes = [];
273
+ const d = signals.signals.discoverability.indicators;
274
+ if (d.publishable) {
275
+ pts += 5;
276
+ notes.push(`Publishable package (${d.package_name})`);
277
+ }
278
+ else {
279
+ notes.push('Not a publishable npm package (private or no name)');
280
+ }
281
+ if (d.readme_mentions_install && d.readme_mentions_mcp) {
282
+ pts += 5;
283
+ notes.push('README documents agent install (MCP + install command)');
284
+ }
285
+ else if (d.readme_mentions_install) {
286
+ pts += 2;
287
+ notes.push('README documents install but no MCP framing');
288
+ }
289
+ else {
290
+ notes.push('README missing agent-install instructions');
291
+ }
292
+ // Public registry signal — proxy: homepage + repository fields populated
293
+ if (d.has_homepage && d.has_repository_field) {
294
+ pts += 5;
295
+ notes.push('Homepage + repository fields populated (registry-discoverable)');
296
+ }
297
+ else {
298
+ notes.push('Missing homepage and/or repository field');
299
+ }
300
+ return { pts: Math.min(pts, max), max, status: statusFor(pts, max), notes };
301
+ }
302
+ function flagAntiPatterns(signals) {
303
+ const flags = [];
304
+ if (!signals.derived.has_any_agent_interface) {
305
+ flags.push({
306
+ slug: 'invisible-product',
307
+ name: 'The Invisible Product',
308
+ evidence: 'No MCP server, no CLI, no typed SDK detected.',
309
+ });
310
+ }
311
+ if (signals.derived.agents_without_rules_risk) {
312
+ flags.push({
313
+ slug: 'agents-without-rules',
314
+ name: 'Agents Without Rules',
315
+ evidence: 'Agent interface exists but no AGENTS.md at repo root.',
316
+ });
317
+ }
318
+ if (signals.derived.god_server_risk === 'fail') {
319
+ flags.push({
320
+ slug: 'god-server',
321
+ name: 'The God Server',
322
+ evidence: `Estimated ${signals.signals.tool_count.estimated_total} tools — well over the 100-tool fail line.`,
323
+ });
324
+ }
325
+ else if (signals.derived.god_server_risk === 'warn') {
326
+ flags.push({
327
+ slug: 'god-server',
328
+ name: 'The God Server',
329
+ evidence: `Estimated ${signals.signals.tool_count.estimated_total} tools — past the 30-tool warn line.`,
330
+ });
331
+ }
332
+ const mm = signals.signals.multi_model.indicators;
333
+ if (mm.distinct_llm_sdks.length === 1 &&
334
+ !signals.signals.multi_model.detected) {
335
+ flags.push({
336
+ slug: 'single-model-trust',
337
+ name: 'Single-Model Trust',
338
+ evidence: `Only ${mm.distinct_llm_sdks[0]} SDK in the tree; no consensus pattern wiring high-stakes decisions.`,
339
+ });
340
+ }
341
+ // Ship-and-forget: agent files older than 180 days while repo presumably moves
342
+ const ages = signals.signals.staleness.agent_file_ages;
343
+ const oldAgentFiles = Object.entries(ages).filter(([, a]) => a.days_ago > 180);
344
+ if (oldAgentFiles.length > 0 && Object.keys(ages).length > 0) {
345
+ flags.push({
346
+ slug: 'ship-and-forget',
347
+ name: 'Ship and Forget',
348
+ evidence: `Agent files untouched for 180+ days: ${oldAgentFiles.map(([f, a]) => `${f} (${a.days_ago}d)`).join(', ')}`,
349
+ });
350
+ }
351
+ return flags;
352
+ }
353
+ function computeTopMoves(principles) {
354
+ const gaps = [];
355
+ for (const slug of PRINCIPLE_SLUGS) {
356
+ const s = principles[slug];
357
+ if (!s)
358
+ continue;
359
+ const gap = s.max - s.pts;
360
+ if (gap > 0)
361
+ gaps.push({ slug, gap });
362
+ }
363
+ // Discoverability counts too
364
+ const disc = principles['discoverability'];
365
+ if (disc) {
366
+ const gap = disc.max - disc.pts;
367
+ if (gap > 0)
368
+ gaps.push({ slug: 'discoverability', gap });
369
+ }
370
+ gaps.sort((a, b) => b.gap - a.gap);
371
+ return gaps.slice(0, 3).map((g, i) => ({
372
+ rank: i + 1,
373
+ principle: g.slug,
374
+ gap_pts: g.gap,
375
+ action: actionFor(g.slug),
376
+ }));
377
+ }
378
+ function actionFor(slug) {
379
+ if (slug === 'discoverability') {
380
+ return 'Publish to npm under @capitalthought/* (or your scope), populate package.json homepage + repository, and document `npx -y <pkg>` agent install in the README.';
381
+ }
382
+ return SMALLEST_EXPERIMENT[slug] ?? 'Apply the corresponding principle from https://agentsfirst.dev/principles/.';
383
+ }
384
+ // ─── Website scoring ──────────────────────────────────────────────────────────
385
+ export function scoreWebsite(signals) {
386
+ const dimensions = {
387
+ discoverability: scoreWebDiscoverability(signals),
388
+ 'content-accessibility': scoreContentAccessibility(signals),
389
+ 'bot-access-control': scoreBotAccessControl(signals),
390
+ 'agent-capabilities': scoreAgentCapabilities(signals),
391
+ 'visibility-of-agent-integrations': scoreVisibilityIntegrations(signals),
392
+ };
393
+ const score = Object.values(dimensions).reduce((a, b) => a + b.pts, 0);
394
+ const level = levelFor(score);
395
+ const anti_patterns_flagged = [];
396
+ // Invisible product if no agent surface at all
397
+ const cap = dimensions['agent-capabilities'];
398
+ if (cap && cap.pts === 0) {
399
+ anti_patterns_flagged.push({
400
+ slug: 'invisible-product',
401
+ name: 'The Invisible Product',
402
+ evidence: 'No MCP Server Card, no CLI/SDK reference, no agent capability surfaces detected.',
403
+ });
404
+ }
405
+ // Agents without rules — agent surfaces exist but no AGENTS.md / well-known/agent-rules
406
+ const surfaces = signals.signals.surfaces;
407
+ const hasAgentMd = surfaces['agents_md']?.ok || surfaces['well_known_agent_rules']?.ok;
408
+ if (cap && cap.pts > 0 && !hasAgentMd) {
409
+ anti_patterns_flagged.push({
410
+ slug: 'agents-without-rules',
411
+ name: 'Agents Without Rules',
412
+ evidence: 'Agent capabilities advertised but no /AGENTS.md or /.well-known/agent-rules.',
413
+ });
414
+ }
415
+ const top_moves = computeWebsiteTopMoves(dimensions);
416
+ return {
417
+ score,
418
+ level,
419
+ level_name: LEVEL_NAMES[level],
420
+ dimensions,
421
+ anti_patterns_flagged,
422
+ top_moves,
423
+ probe_signals: signals,
424
+ };
425
+ }
426
+ function scoreWebDiscoverability(signals) {
427
+ const max = 25;
428
+ let pts = 0;
429
+ const notes = [];
430
+ const surfaces = signals.signals.surfaces;
431
+ const robots = signals.signals.robots_analysis;
432
+ if (robots && robots.address_count > 0) {
433
+ pts += 5;
434
+ notes.push(`robots.txt addresses ${robots.address_count} AI agents: ${robots.ai_agents_addressed.join(', ')}`);
435
+ }
436
+ else if (surfaces['robots_txt']?.ok) {
437
+ notes.push('robots.txt exists but does not address AI agents specifically');
438
+ }
439
+ else {
440
+ notes.push('No robots.txt');
441
+ }
442
+ if (surfaces['llms_txt']?.ok || surfaces['llms_full_txt']?.ok) {
443
+ pts += 10;
444
+ notes.push('/llms.txt published');
445
+ }
446
+ else {
447
+ notes.push('No /llms.txt');
448
+ }
449
+ if (surfaces['agents_md']?.ok || surfaces['well_known_agent_rules']?.ok) {
450
+ pts += 10;
451
+ notes.push('/AGENTS.md or /.well-known/agent-rules published');
452
+ }
453
+ else {
454
+ notes.push('No /AGENTS.md or /.well-known/agent-rules');
455
+ }
456
+ return { pts: Math.min(pts, max), max, status: statusFor(pts, max), notes };
457
+ }
458
+ function scoreContentAccessibility(signals) {
459
+ const max = 20;
460
+ let pts = 0;
461
+ const notes = [];
462
+ if (signals.signals.markdown_negotiation.served_markdown) {
463
+ pts += 10;
464
+ notes.push('Server responds to text/markdown content negotiation');
465
+ }
466
+ else {
467
+ notes.push('No markdown content negotiation');
468
+ }
469
+ if (signals.signals.surfaces['sitemap']?.ok) {
470
+ pts += 5;
471
+ notes.push('sitemap.xml present');
472
+ }
473
+ else {
474
+ notes.push('No sitemap.xml');
475
+ }
476
+ const openapiSurfaces = ['openapi_root', 'openapi_v1', 'openapi_api'].some((k) => signals.signals.surfaces[k]?.ok);
477
+ if (openapiSurfaces) {
478
+ pts += 5;
479
+ notes.push('OpenAPI surface discoverable');
480
+ }
481
+ else {
482
+ notes.push('No discoverable OpenAPI surface');
483
+ }
484
+ return { pts: Math.min(pts, max), max, status: statusFor(pts, max), notes };
485
+ }
486
+ function scoreBotAccessControl(signals) {
487
+ const max = 15;
488
+ let pts = 0;
489
+ const notes = [];
490
+ const robots = signals.signals.robots_analysis;
491
+ if (robots) {
492
+ if (robots.address_count > 0 && !robots.blanket_disallow) {
493
+ pts += 10;
494
+ notes.push('Per-bot allow/deny posture (no blanket disallow)');
495
+ }
496
+ else if (robots.address_count > 0) {
497
+ pts += 5;
498
+ notes.push('Addresses AI agents but uses blanket disallow');
499
+ }
500
+ if (robots.address_count >= 3) {
501
+ pts += 5;
502
+ notes.push(`Distinct posture for ${robots.address_count} bots — granular control`);
503
+ }
504
+ }
505
+ else {
506
+ notes.push('No robots.txt analysis available');
507
+ }
508
+ return { pts: Math.min(pts, max), max, status: statusFor(pts, max), notes };
509
+ }
510
+ function scoreAgentCapabilities(signals) {
511
+ const max = 30;
512
+ let pts = 0;
513
+ const notes = [];
514
+ const surfaces = signals.signals.surfaces;
515
+ if (surfaces['well_known_mcp']?.ok) {
516
+ pts += 15;
517
+ notes.push('/.well-known/mcp-server-card published');
518
+ }
519
+ else {
520
+ notes.push('No MCP Server Card');
521
+ }
522
+ const homepage = signals.signals.homepage_analysis;
523
+ if (homepage) {
524
+ if (homepage.mentions_mcp || homepage.mentions_cli || homepage.mentions_sdk) {
525
+ pts += 10;
526
+ const which = [];
527
+ if (homepage.mentions_mcp)
528
+ which.push('MCP');
529
+ if (homepage.mentions_cli)
530
+ which.push('CLI');
531
+ if (homepage.mentions_sdk)
532
+ which.push('SDK');
533
+ notes.push(`Homepage references agent surfaces: ${which.join(', ')}`);
534
+ }
535
+ else {
536
+ notes.push('Homepage does not reference MCP / CLI / SDK');
537
+ }
538
+ }
539
+ if (surfaces['well_known_oauth']?.ok || surfaces['well_known_ai_plugin']?.ok) {
540
+ pts += 5;
541
+ notes.push('OAuth / AI-plugin auth-server discovery present');
542
+ }
543
+ else {
544
+ notes.push('No OAuth-with-PKCE or ai-plugin discovery');
545
+ }
546
+ return { pts: Math.min(pts, max), max, status: statusFor(pts, max), notes };
547
+ }
548
+ function scoreVisibilityIntegrations(signals) {
549
+ const max = 10;
550
+ let pts = 0;
551
+ const notes = [];
552
+ const homepage = signals.signals.homepage_analysis;
553
+ if (homepage) {
554
+ const mentionsAny = homepage.mentions_mcp || homepage.mentions_npx || homepage.mentions_cli;
555
+ if (mentionsAny) {
556
+ pts += 10;
557
+ notes.push('Homepage promotes agent install / MCP / CLI alongside human onboarding');
558
+ }
559
+ else {
560
+ notes.push('Homepage hides agent integrations from human-onboarding flow');
561
+ }
562
+ }
563
+ else {
564
+ notes.push('Homepage body unavailable');
565
+ }
566
+ return { pts, max, status: statusFor(pts, max), notes };
567
+ }
568
+ function computeWebsiteTopMoves(dimensions) {
569
+ const gaps = [];
570
+ if (dimensions['discoverability']?.pts !== dimensions['discoverability']?.max) {
571
+ gaps.push({
572
+ slug: 'discoverability',
573
+ gap: (dimensions['discoverability']?.max ?? 0) - (dimensions['discoverability']?.pts ?? 0),
574
+ action: 'Publish /llms.txt and /AGENTS.md at the site root. Update robots.txt to address GPTBot, ClaudeBot, anthropic-ai, Google-Extended, PerplexityBot, CCBot explicitly.',
575
+ });
576
+ }
577
+ if (dimensions['agent-capabilities']?.pts !== dimensions['agent-capabilities']?.max) {
578
+ gaps.push({
579
+ slug: 'agent-capabilities',
580
+ gap: (dimensions['agent-capabilities']?.max ?? 0) - (dimensions['agent-capabilities']?.pts ?? 0),
581
+ action: 'Ship an MCP server (verb-first tools, Zod params), publish /.well-known/mcp-server-card, and reference it from the homepage hero.',
582
+ });
583
+ }
584
+ if (dimensions['content-accessibility']?.pts !== dimensions['content-accessibility']?.max) {
585
+ gaps.push({
586
+ slug: 'content-accessibility',
587
+ gap: (dimensions['content-accessibility']?.max ?? 0) - (dimensions['content-accessibility']?.pts ?? 0),
588
+ action: 'Serve text/markdown when the Accept header asks for it. Publish sitemap.xml and an OpenAPI document at /openapi.json.',
589
+ });
590
+ }
591
+ if (dimensions['bot-access-control']?.pts !== dimensions['bot-access-control']?.max) {
592
+ gaps.push({
593
+ slug: 'bot-access-control',
594
+ gap: (dimensions['bot-access-control']?.max ?? 0) - (dimensions['bot-access-control']?.pts ?? 0),
595
+ action: 'Replace any blanket Disallow with per-bot rules. Cloudflare Content Signals or equivalent declarative AI-policy.',
596
+ });
597
+ }
598
+ if (dimensions['visibility-of-agent-integrations']?.pts !==
599
+ dimensions['visibility-of-agent-integrations']?.max) {
600
+ gaps.push({
601
+ slug: 'visibility-of-agent-integrations',
602
+ gap: (dimensions['visibility-of-agent-integrations']?.max ?? 0) -
603
+ (dimensions['visibility-of-agent-integrations']?.pts ?? 0),
604
+ action: 'Add an "Install our MCP server" or "Use our CLI" call-out next to the human-onboarding hero, not buried in /docs.',
605
+ });
606
+ }
607
+ gaps.sort((a, b) => b.gap - a.gap);
608
+ return gaps.slice(0, 3).map((g, i) => ({
609
+ rank: i + 1,
610
+ principle: g.slug,
611
+ gap_pts: g.gap,
612
+ action: g.action,
613
+ }));
614
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};