@bbradar/mcp 0.2.5 → 0.2.6

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/server.js CHANGED
@@ -5,8 +5,8 @@ import { BBRadarApiError } from "./bbradarClient.js";
5
5
  import { NumericCursorStore } from "./cursorStore.js";
6
6
  import { stableStringify, stripUndefined } from "./json.js";
7
7
  import { FixedWindowRateLimiter } from "./rateLimit.js";
8
- import { readArray, readBoolean, readNumber, readObject, readString, sanitizeJson, sanitizeChange, sanitizeProgram, sanitizeRepositoryChange, sanitizeTarget, sanitizeTargetsForExport } from "./sanitize.js";
9
- const SERVER_VERSION = "0.2.5";
8
+ import { readArray, readBoolean, readNumber, readObject, readString, sanitizeJson, sanitizeProPayload, sanitizeChange, sanitizeProgram, sanitizeRepositoryChange, sanitizeTarget, sanitizeTargetsForExport } from "./sanitize.js";
9
+ const SERVER_VERSION = "0.2.6";
10
10
  const MAX_PROGRAMS_PER_SEARCH = 100;
11
11
  const MAX_TARGET_EXPORT = 500;
12
12
  const MAX_RECENT_CHANGES = 100;
@@ -64,7 +64,14 @@ const CORE_TOOL_NAMES = new Set([
64
64
  "list_filters",
65
65
  "get_bbradar_guide",
66
66
  "get_mcp_status",
67
- "export_targets"
67
+ "export_targets",
68
+ "get_history_changes",
69
+ "get_history_change_detail",
70
+ "get_program_history",
71
+ "get_program_activity",
72
+ "get_analytics_platform_series",
73
+ "get_analytics_program_activity",
74
+ "get_analytics_program_detail"
68
75
  ]);
69
76
  const TOOL_ENDPOINT_REQUIREMENTS = {
70
77
  search_targets: "target_search",
@@ -97,7 +104,6 @@ const PROGRAM_ID_PLATFORM_GUESSES = [
97
104
  ];
98
105
  const PRIMARY_PROGRAM_ID_PLATFORM_GUESSES = PROGRAM_ID_PLATFORM_GUESSES.slice(0, 8);
99
106
  const WEB3_TAGS = ["web3", "crypto", "blockchain", "smart-contract", "smart contract", "defi", "nft", "ethereum", "solana", "solidity"];
100
- const WEB3_CONTEST_SIGNALS = ["contest", "audit", "competitive", "code4rena", "sherlock", "cantina", "codehawks", "hats", "spearbit", "warden"];
101
107
  const VDP_SIGNALS = ["vdp", "disclosure", "responsible disclosure", "vulnerability disclosure", "no bounty", "non-bounty", "no reward"];
102
108
  const PROGRAM_RESOLUTION_STOP_WORDS = new Set([
103
109
  "program",
@@ -254,6 +260,9 @@ const stableSnapshotSchema = z
254
260
  message: "Snapshot tokens cannot contain control characters."
255
261
  })
256
262
  .describe("Optional stable-page token. Send 'start' on page 1, then reuse meta.snapshot with identical filters. Snapshot creation is consistency-first and can be slower; omit it for the fastest live result.");
263
+ const opaqueCursorSchema = z.string().min(1).max(4096).describe("Opaque five-minute cursor. Reuse with identical filters and page size; restart the query on expiry.");
264
+ const analyticsDateSchema = () => z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Use an inclusive UTC YYYY-MM-DD date.");
265
+ const proLimitSchema = z.number().int().min(1).max(100);
257
266
  const pageSchema = z.number().int().min(1).default(1);
258
267
  const programsPageSizeSchema = z.number().int().min(1).max(MAX_PROGRAMS_PER_SEARCH).default(20);
259
268
  const recentChangesPageSizeSchema = z.number().int().min(1).max(MAX_RECENT_CHANGES).default(50);
@@ -278,7 +287,7 @@ const programIdSchema = z
278
287
  .string()
279
288
  .trim()
280
289
  .min(3)
281
- .max(256)
290
+ .max(620)
282
291
  .regex(/^[^:/?#\u0000-\u001F\u007F]+:[^?#\u0000-\u001F\u007F]+$/, "Use platform:handle, for example HackerOne:example-handle.")
283
292
  .refine((value) => !/[\u0000-\u001F\u007F]/.test(value), {
284
293
  message: "program_id cannot contain control characters."
@@ -313,6 +322,7 @@ function cacheOutputSchema() {
313
322
  hit: z.boolean(),
314
323
  stale_while_revalidate: z.boolean().optional(),
315
324
  coalesced_live_request: z.boolean().optional(),
325
+ no_store: z.boolean().optional(),
316
326
  expires_at: z.string().optional()
317
327
  });
318
328
  }
@@ -338,6 +348,7 @@ function apiEnvelopeOutputShape() {
338
348
  mcp_timing: toolTimingOutputSchema().optional(),
339
349
  warnings: z.array(z.string()).optional(),
340
350
  program_id_resolution: jsonRecordSchema().optional(),
351
+ recovery: jsonRecordSchema().optional(),
341
352
  error: errorOutputSchema().optional()
342
353
  };
343
354
  }
@@ -400,7 +411,11 @@ const programCandidatePresetInputSchema = {
400
411
  };
401
412
  function createProfiledToolRegistrar(server, profile, client) {
402
413
  return (name, toolConfig, callback) => {
403
- const registeredTool = server.registerTool(name, toolConfig, callback);
414
+ const scopedCallback = ((...args) => {
415
+ const extra = args[args.length - 1];
416
+ return client.withRequestContext(extra.signal, () => callback(...args), readObject(args[0])?.snapshot === "start");
417
+ });
418
+ const registeredTool = server.registerTool(name, toolConfig, scopedCallback);
404
419
  const requiredEndpoint = TOOL_ENDPOINT_REQUIREMENTS[name];
405
420
  if ((profile === "core" && !CORE_TOOL_NAMES.has(name)) ||
406
421
  (requiredEndpoint !== undefined && !client.supportsEndpoint(requiredEndpoint))) {
@@ -417,6 +432,7 @@ export function createBbradarServer(client, config) {
417
432
  "Use resolve_program or run_program_name_action for a named program.",
418
433
  "Use find_hunt_targets once for requests combining good rewards, broad scope, target quality, and recent target additions. Use find_repository_hunt_targets instead when Git or repository activity is explicitly required.",
419
434
  "Use find_programs for lower-level program-only discovery and find_programs_by_target for reverse target lookup.",
435
+ "For semantic changes use get_history_changes or get_program_history, then get_history_change_detail only when before/after items are needed. For exact activity use get_program_activity; for ranked activity use get_analytics_program_activity. Use daily analytics tools directly for date series.",
420
436
  "Default to bounty-eligible in-scope data unless the user explicitly requests broader results.",
421
437
  "Prefer compact/identifier outputs and disclose sampled ranking scope when present."
422
438
  ]
@@ -432,6 +448,7 @@ export function createBbradarServer(client, config) {
432
448
  "Self-Hosted programs use Self-Hosted:slug ids and are available through program search, exact detail, and target tools. Respect data_capabilities; do not infer missing opportunity, report-count, target-change, submission-cost, or repository signals.",
433
449
  "Freshness: find_recently_updated_programs, get_latest_added_targets, check_program_new_targets, check_watchlist_new_targets, find_recent_by_type, get_program_scope_delta.",
434
450
  "Repository activity: get_repository_changes defaults to compact program-only events. Seed with after_id, then resume the in-memory per-filter cursor.",
451
+ "Semantic History: get_history_changes globally or get_program_history for one exact ID, especially Self-Hosted. Fetch detail lazily with get_history_change_detail. Exact recorded signals: get_program_activity. Server-side rankings: get_analytics_program_activity. Daily series: get_analytics_platform_series or get_analytics_program_detail. Reuse opaque cursors only with unchanged filters and page size.",
435
452
  "Discovery: find_hunt_targets is the one-call default; find_program_candidates, find_wildcard_programs, find_recently_added_wildcards, find_low_competition_high_reward, find_stack_matches, find_language_programs, find_target_type_programs, find_reward_programs, find_paid_programs, find_vdp_programs, find_web3_contests, and find_low_noise_programs are lower-level alternatives.",
436
453
  "Details: get_program_brief, get_program_scope_summary, get_program_target_breakdown, find_programs_by_target, compare_programs_compact.",
437
454
  "Prefer compact/identifier outputs and disclose sampled ranking scope when present."
@@ -459,11 +476,88 @@ export function createBbradarServer(client, config) {
459
476
  const repositoryChangesPageSizeSchema = z.number().int().min(1).max(maxRepositoryChangesPageSize).default(Math.min(50, maxRepositoryChangesPageSize));
460
477
  const acceptedTargetLimitSchema = z.number().int().min(1).max(maxProgramTargetsPageSize);
461
478
  const targetSearchPageSizeSchema = z.number().int().min(1).max(maxTargetSearchPageSize).default(Math.min(50, maxTargetSearchPageSize));
479
+ const historyOutput = { ...apiEnvelopeOutputShape(), changes: z.array(jsonRecordSchema()).optional(), coverage: jsonRecordSchema().optional(), meta: jsonRecordSchema().optional() };
480
+ const analyticsOutput = { ...apiEnvelopeOutputShape(), platform: z.string().optional(), program: jsonRecordSchema().optional(), programs: z.array(jsonRecordSchema()).optional(), series: z.array(jsonRecordSchema()).optional(), observations: jsonRecordSchema().optional(), category: z.string().optional(), population: z.string().optional(), ranking_definition: z.string().optional(), inventory_definition: z.string().optional(), meta: jsonRecordSchema().optional() };
481
+ registerTool("get_history_changes", {
482
+ title: "Get Program History Changes",
483
+ description: "One page of semantic program History. Use get_program_history for an exact program, especially Self-Hosted. History may be partial or unavailable; inspect coverage.",
484
+ inputSchema: {
485
+ cursor: opaqueCursorSchema.optional(), days: z.number().int().min(0).max(3650).default(90),
486
+ platform: z.string().min(1).max(100).optional(), program_id: programIdSchema.optional(),
487
+ category: z.enum(["scope", "rewards", "rules", "program", "repositories"]).optional(),
488
+ search: z.string().min(1).max(120).optional(), limit: proLimitSchema.default(50)
489
+ }, outputSchema: historyOutput, annotations: readOnlyAnnotations
490
+ }, (args) => runTool("get_history_changes", config, rateLimiter, async () => {
491
+ assertProFeature(client, "history_changes", "history_capture_enabled");
492
+ if (args.days === 0 && !args.program_id)
493
+ throw new Error("days=0 requires program_id; use get_program_history for Self-Hosted catalog IDs.");
494
+ const api = await client.getHistoryChanges(toQuery(args));
495
+ return proPage(api, "changes");
496
+ }));
497
+ registerTool("get_history_change_detail", {
498
+ title: "Get History Change Detail",
499
+ description: "Fetch one semantic History change's paginated before/after items only when needed.",
500
+ inputSchema: { change_id: z.string().min(1).max(128), cursor: opaqueCursorSchema.optional(), limit: proLimitSchema.default(50) },
501
+ outputSchema: { ...apiEnvelopeOutputShape(), change: jsonRecordSchema().optional(), items: z.array(jsonRecordSchema()).optional(), meta: jsonRecordSchema().optional() }, annotations: readOnlyAnnotations
502
+ }, (args) => runTool("get_history_change_detail", config, rateLimiter, async () => {
503
+ assertProFeature(client, "history_change_detail");
504
+ assertHistoryCapture(client);
505
+ return proPage(await client.getHistoryChangeDetail(args.change_id, toQuery({ cursor: args.cursor, limit: args.limit })), "items");
506
+ }));
507
+ registerTool("get_program_history", {
508
+ title: "Get Program History",
509
+ description: "One page of exact semantic History for a platform:handle or Self-Hosted:slug catalog ID. Preserve archive id and catalog_id separately.",
510
+ inputSchema: { program_id: programIdSchema, cursor: opaqueCursorSchema.optional(), days: z.number().int().min(0).max(3650).default(90), limit: proLimitSchema.default(50) },
511
+ outputSchema: historyOutput, annotations: readOnlyAnnotations
512
+ }, (args) => runTool("get_program_history", config, rateLimiter, async () => {
513
+ assertProFeature(client, "program_history");
514
+ assertHistoryCapture(client);
515
+ return proPage(await client.getProgramHistory(args.program_id, toQuery({ cursor: args.cursor, days: args.days, limit: args.limit })), "changes");
516
+ }));
517
+ registerTool("get_program_activity", {
518
+ title: "Get Exact Program Activity",
519
+ description: "Read the authoritative activity_summary for one exact program ID.",
520
+ inputSchema: { program_id: programIdSchema },
521
+ outputSchema: { ...apiEnvelopeOutputShape(), program_id: z.string().optional(), activity_summary: jsonRecordSchema().optional() }, annotations: readOnlyAnnotations
522
+ }, (args) => runTool("get_program_activity", config, rateLimiter, async () => {
523
+ assertProFeature(client, "program_activity");
524
+ return proPage(await client.getProgramActivity(args.program_id), "activity_summary");
525
+ }));
526
+ registerTool("get_analytics_platform_series", {
527
+ title: "Get Platform Daily Analytics",
528
+ description: "One page of inclusive UTC daily platform analytics; inspect generation, projection_pending, and tracking_since.",
529
+ inputSchema: { platform: z.string().min(1).max(100), start: analyticsDateSchema().optional(), end: analyticsDateSchema().optional(), cursor: opaqueCursorSchema.optional(), limit: proLimitSchema.default(100) },
530
+ outputSchema: analyticsOutput, annotations: readOnlyAnnotations
531
+ }, (args) => runTool("get_analytics_platform_series", config, rateLimiter, async () => {
532
+ assertProFeature(client, "analytics_platform_series", "enabled");
533
+ const { platform, ...query } = args;
534
+ return proPage(await client.getAnalyticsPlatformSeries(platform, toQuery(query)), "series");
535
+ }));
536
+ registerTool("get_analytics_program_activity", {
537
+ title: "Rank Programs by Recorded Activity",
538
+ description: "Server-side daily activity ranking. One page; semantic activity excludes baselines and corrections.",
539
+ inputSchema: { start: analyticsDateSchema().optional(), end: analyticsDateSchema().optional(), platform: z.string().min(1).max(100).optional(), category: z.enum(["all", "scope", "rewards", "rules", "schedule", "costs", "lifecycle"]).default("all"), cursor: opaqueCursorSchema.optional(), limit: proLimitSchema.default(50) },
540
+ outputSchema: analyticsOutput, annotations: readOnlyAnnotations
541
+ }, (args) => runTool("get_analytics_program_activity", config, rateLimiter, async () => {
542
+ assertProFeature(client, "analytics_program_activity", "enabled");
543
+ return proPage(await client.getAnalyticsProgramActivity(toQuery(args)), "programs");
544
+ }));
545
+ registerTool("get_analytics_program_detail", {
546
+ title: "Get Program Daily Analytics",
547
+ description: "One page of program daily metrics, carried inventory state, report observations, and source coverage.",
548
+ inputSchema: { program_id: programIdSchema, start: analyticsDateSchema().optional(), end: analyticsDateSchema().optional(), cursor: opaqueCursorSchema.optional(), limit: proLimitSchema.default(100) },
549
+ outputSchema: analyticsOutput, annotations: readOnlyAnnotations
550
+ }, (args) => runTool("get_analytics_program_detail", config, rateLimiter, async () => {
551
+ assertProFeature(client, "analytics_program_detail", "enabled");
552
+ const { program_id, ...query } = args;
553
+ return proPage(await client.getAnalyticsProgramDetail(program_id, toQuery(query)), "series");
554
+ }));
462
555
  registerTool("search_programs", {
463
556
  title: "Search BBRadar Programs",
464
557
  description: `Search or list active and confirmed paused platform programs plus active Self-Hosted programs. page_size max ${maxProgramsPageSize}. Stable snapshots are opt-in because creating one can be slower than live indexed pagination.`,
465
558
  inputSchema: {
466
559
  search: searchTextSchema.optional().describe("Server-side program name, handle, platform, policy-host, or target search."),
560
+ program_kind: z.enum(["bounty", "contest", "challenge"]).optional(),
467
561
  platforms: platformListSchema(),
468
562
  tags: tagListSchema(),
469
563
  updated_since: isoDateTimeSchema.optional().describe("ISO datetime."),
@@ -718,16 +812,16 @@ export function createBbradarServer(client, config) {
718
812
  }, (args) => runTool("get_recent_changes", config, rateLimiter, async () => {
719
813
  const { output_mode, resume_cursor, max_cursor_pages, cursor_time_budget_ms, ...filters } = args;
720
814
  assertStableSnapshotSupported(client, "target_changes", filters.snapshot);
721
- const cursor = resolveFeedCursor(feedCursorStore, "target_changes", filters, resume_cursor);
815
+ const cursor = resolveFeedCursor(feedCursorStore, "target_changes", filters, resume_cursor, config);
722
816
  const { min_target_score, target_opportunity_label, competition_risk, snapshot, ...upstreamFilters } = filters;
723
- const collected = await collectCursorFeedPages(feedCursorStore, cursor, max_cursor_pages, cursor_time_budget_ms, snapshot, (afterId, recoveryProbe, activeSnapshot) => client.getRecentChanges(toQuery({
817
+ const collected = await collectCursorFeedPages(feedCursorStore, cursor, max_cursor_pages, cursor_time_budget_ms, snapshot, filters.page, (afterId, recoveryProbe, activeSnapshot, pageNumber) => client.getRecentChanges(toQuery({
724
818
  ...upstreamFilters,
725
819
  minTargetScore: min_target_score,
726
820
  targetOpportunityLabel: target_opportunity_label,
727
821
  competitionRisk: competition_risk,
728
822
  snapshot: activeSnapshot,
729
823
  after_id: afterId,
730
- page: afterId === undefined ? filters.page : 1
824
+ page: activeSnapshot ? pageNumber : afterId === undefined ? filters.page : 1
731
825
  }), recoveryProbe ? { cache: false, coalesce: false } : {}), (startPage, recoveryProbe) => refreshInventoryBaseline((page, pageProbe) => client.searchTargets(toQuery({
732
826
  q: upstreamFilters.search,
733
827
  platforms: upstreamFilters.platforms,
@@ -795,13 +889,13 @@ export function createBbradarServer(client, config) {
795
889
  }
796
890
  const { output_mode, resume_cursor, max_cursor_pages, cursor_time_budget_ms, ...filters } = args;
797
891
  assertStableSnapshotSupported(client, "repository_changes", filters.snapshot);
798
- const cursor = resolveFeedCursor(feedCursorStore, "repository_changes", filters, resume_cursor);
892
+ const cursor = resolveFeedCursor(feedCursorStore, "repository_changes", filters, resume_cursor, config);
799
893
  const { snapshot, ...upstreamFilters } = filters;
800
- const collected = await collectCursorFeedPages(feedCursorStore, cursor, max_cursor_pages, cursor_time_budget_ms, snapshot, (afterId, recoveryProbe, activeSnapshot) => client.getRepositoryChanges(toQuery({
894
+ const collected = await collectCursorFeedPages(feedCursorStore, cursor, max_cursor_pages, cursor_time_budget_ms, snapshot, filters.page, (afterId, recoveryProbe, activeSnapshot, pageNumber) => client.getRepositoryChanges(toQuery({
801
895
  ...upstreamFilters,
802
896
  snapshot: activeSnapshot,
803
897
  after_id: afterId,
804
- page: afterId === undefined ? filters.page : 1
898
+ page: activeSnapshot ? pageNumber : afterId === undefined ? filters.page : 1
805
899
  }), recoveryProbe ? { cache: false, coalesce: false } : {}), (startPage, recoveryProbe) => refreshInventoryBaseline((page, pageProbe) => client.getRepositoryChanges(toQuery({
806
900
  ...upstreamFilters,
807
901
  after_id: undefined,
@@ -1130,6 +1224,7 @@ export function createBbradarServer(client, config) {
1130
1224
  target_types: targetTypeListSchema(),
1131
1225
  language_tags: languageTagListSchema(),
1132
1226
  web3: z.boolean().default(false).describe("Require Web3 signals."),
1227
+ program_kind: z.enum(["bounty", "contest", "challenge"]).optional().describe("Source-classified program kind; sent to the Pro program list."),
1133
1228
  exclude_web3: z.boolean().default(false).describe("Exclude programs with Web3, blockchain, smart-contract, crypto, DeFi, NFT, Solidity, Ethereum, or Solana signals. Use this for Web2-only discovery."),
1134
1229
  opportunity_levels: z.array(opportunityLevelSchema).max(4).default([]),
1135
1230
  updated_since: isoDateTimeSchema.optional(),
@@ -1144,7 +1239,7 @@ export function createBbradarServer(client, config) {
1144
1239
  exclude_mobile_only: z.boolean().default(false),
1145
1240
  exclude_non_web: z.boolean().default(false),
1146
1241
  has_api_scope: z.boolean().default(false),
1147
- contest_like: z.boolean().default(false).describe("Require contest/audit signals."),
1242
+ contest_like: z.boolean().default(false).describe("Require source-classified contests."),
1148
1243
  vdp_mode: vdpModeSchema.default("exclude").describe("VDP/no-bounty filter mode."),
1149
1244
  fresh_launch_days: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).optional(),
1150
1245
  min_wildcards: z.number().int().min(0).max(1000).default(0),
@@ -1772,7 +1867,8 @@ export function createBbradarServer(client, config) {
1772
1867
  ...apiEnvelopeOutputShape(),
1773
1868
  program: programOutputSchema().optional(),
1774
1869
  activity: jsonRecordSchema().optional(),
1775
- changes: z.array(jsonRecordSchema()).optional()
1870
+ changes: z.array(jsonRecordSchema()).optional(),
1871
+ meta: jsonRecordSchema().optional()
1776
1872
  },
1777
1873
  annotations: readOnlyAnnotations
1778
1874
  }, (args) => runTool("summarize_program_activity", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => summarizeProgramActivity(client, config, programId, args.recent_changes_limit))));
@@ -1976,13 +2072,15 @@ export function createBbradarServer(client, config) {
1976
2072
  const exportId = randomUUID();
1977
2073
  const sanitizedExport = sanitizeTargetsForExport(api.data, args.limit);
1978
2074
  const resourceUri = exportResourceUri(exportId);
1979
- rememberExport(exportStore, exportId, sanitizedExport);
2075
+ if (!api.noStore)
2076
+ rememberExport(exportStore, exportId, sanitizedExport);
1980
2077
  return withApiMetadata(api, {
1981
2078
  source_requests: resolvedProgramIds.sourceRequests.length > 0 ? resolvedProgramIds.sourceRequests : undefined,
1982
2079
  warnings: resolvedProgramIds.warnings.length > 0 ? resolvedProgramIds.warnings : undefined,
1983
2080
  export: {
1984
- export_id: exportId,
1985
- resource_uri: resourceUri,
2081
+ export_id: api.noStore ? undefined : exportId,
2082
+ resource_uri: api.noStore ? undefined : resourceUri,
2083
+ data: api.noStore ? sanitizedExport : undefined,
1986
2084
  preview: previewExportPayload(sanitizedExport),
1987
2085
  returned_preview_count: previewExportCount(sanitizedExport),
1988
2086
  limit: args.limit
@@ -2079,6 +2177,9 @@ function bbradarGuide(topic, profile, client) {
2079
2177
  arguments: { include_targets: false, max_cursor_pages: 2 },
2080
2178
  then: "For incremental polling, seed once with after_id and later set resume_cursor=true with the same filters."
2081
2179
  },
2180
+ { user_goal: "semantic program change history", first_tool: "get_program_history", arguments: { program_id: "Platform:handle" }, then: "Fetch get_history_change_detail only for a selected change." },
2181
+ { user_goal: "most active programs in a date range", first_tool: "get_analytics_program_activity", arguments: {}, then: "Use this server-side ranking rather than sampling the program catalog." },
2182
+ { user_goal: "exact recorded activity for one program", first_tool: "get_program_activity", arguments: { program_id: "Platform:handle" } },
2082
2183
  {
2083
2184
  user_goal: "hunt targets combining repository activity with rewards, target quality, platforms, or Web2/Web3 constraints",
2084
2185
  first_tool: "find_repository_hunt_targets",
@@ -2221,6 +2322,9 @@ function coreBbradarGuide(topic, client) {
2221
2322
  first_tool: "get_repository_changes",
2222
2323
  arguments: { include_targets: false, max_cursor_pages: 2 }
2223
2324
  },
2325
+ { user_goal: "semantic program change history", first_tool: "get_program_history", arguments: { program_id: "Platform:handle" }, then: "Fetch change detail only when needed." },
2326
+ { user_goal: "most active programs", first_tool: "get_analytics_program_activity", arguments: {} },
2327
+ { user_goal: "exact recorded activity", first_tool: "get_program_activity", arguments: { program_id: "Platform:handle" } },
2224
2328
  {
2225
2329
  user_goal: "hunt targets combining repository activity with rewards, target quality, platforms, or Web2/Web3 constraints",
2226
2330
  first_tool: "find_repository_hunt_targets",
@@ -2598,7 +2702,7 @@ function getCachedProgramFallback(config, requestedProgramId) {
2598
2702
  return cloneProgramFallbackForRequest(cached.fallback, requestedProgramId, true);
2599
2703
  }
2600
2704
  function rememberProgramFallback(config, fallback) {
2601
- if (config.cacheTtlMs <= 0 || config.cacheMaxEntries <= 0) {
2705
+ if (config.cacheTtlMs <= 0 || config.cacheMaxEntries <= 0 || hasNoStoreSource(fallback.sourceRequests)) {
2602
2706
  return;
2603
2707
  }
2604
2708
  pruneProgramFallbackCache();
@@ -2623,7 +2727,13 @@ function pruneProgramFallbackCache() {
2623
2727
  }
2624
2728
  }
2625
2729
  function programFallbackCacheKey(config, requestedProgramId) {
2626
- return `${config.apiBaseUrl}\n${normalizeTag(requestedProgramId)}`;
2730
+ return `${config.apiBaseUrl}\n${credentialCacheScope(config)}\n${isStrictProgramId(requestedProgramId) ? requestedProgramId : normalizeTag(requestedProgramId)}`;
2731
+ }
2732
+ function credentialCacheScope(config) {
2733
+ return createHash("sha256").update(config.apiKey).digest("hex");
2734
+ }
2735
+ function hasNoStoreSource(sourceRequests) {
2736
+ return sourceRequests.some((request) => readBoolean(request.no_store) === true);
2627
2737
  }
2628
2738
  function cloneProgramFallbackForRequest(fallback, requestedProgramId, fromCache) {
2629
2739
  const warnings = fromCache
@@ -3077,7 +3187,8 @@ function getCachedResolveProgram(config, input) {
3077
3187
  });
3078
3188
  }
3079
3189
  function rememberResolveProgram(config, input, payload) {
3080
- if (config.cacheTtlMs <= 0 || config.cacheMaxEntries <= 0) {
3190
+ const sourceRequests = readArray(payload.source_requests).filter((request) => readObject(request) !== undefined);
3191
+ if (config.cacheTtlMs <= 0 || config.cacheMaxEntries <= 0 || hasNoStoreSource(sourceRequests)) {
3081
3192
  return;
3082
3193
  }
3083
3194
  pruneResolveProgramCache();
@@ -3103,7 +3214,8 @@ function pruneResolveProgramCache() {
3103
3214
  function resolveProgramCacheKey(config, input) {
3104
3215
  return stableStringify({
3105
3216
  apiBaseUrl: config.apiBaseUrl,
3106
- query: normalizeTag(input.query),
3217
+ credentialScope: credentialCacheScope(config),
3218
+ query: isStrictProgramId(input.query) ? input.query : normalizeTag(input.query),
3107
3219
  platforms: input.platforms.map(normalizeTag).sort(),
3108
3220
  tags: input.tags.map(normalizeTag).sort(),
3109
3221
  opportunity_levels: [...input.opportunity_levels].sort(),
@@ -4311,8 +4423,9 @@ async function comparePrograms(client, config, input) {
4311
4423
  });
4312
4424
  }
4313
4425
  async function summarizeProgramActivity(client, config, programId, recentChangesLimit) {
4314
- const [programApi, changes] = await Promise.all([
4426
+ const [programApi, activityApi, changes] = await Promise.all([
4315
4427
  client.getProgram(programId),
4428
+ client.supportsEndpoint("program_activity") ? client.getProgramActivity(programId) : Promise.resolve(undefined),
4316
4429
  recentChangesLimit > 0
4317
4430
  ? fetchProgramChanges(client, config, programId, recentChangesLimit, true, true, true)
4318
4431
  : Promise.resolve(undefined)
@@ -4329,15 +4442,18 @@ async function summarizeProgramActivity(client, config, programId, recentChanges
4329
4442
  upstream_request_id: programApi.upstreamRequestId,
4330
4443
  ...apiSourceMetadata(programApi)
4331
4444
  },
4445
+ ...(activityApi ? [{ source: "program_activity", request_id: activityApi.requestId, upstream_request_id: activityApi.upstreamRequestId, ...apiSourceMetadata(activityApi) }] : []),
4332
4446
  ...(changes?.sourceRequests ?? [])
4333
4447
  ],
4334
4448
  program,
4335
4449
  activity: stripUndefined({
4450
+ activity_summary: activityApi ? sanitizeProPayload(readObject(activityApi.data)?.activity_summary) : undefined,
4336
4451
  target_counts: readObject(scopeSummary?.target_counts),
4337
4452
  target_activity: targetActivity,
4338
4453
  recent_change_count: changes?.changes.length
4339
4454
  }),
4340
- changes: changes?.changes ?? []
4455
+ changes: changes?.changes ?? [],
4456
+ meta: { target_change_samples_are_semantic_history: false, activity_source: activityApi ? "pro_activity_endpoint" : "legacy_program_projection" }
4341
4457
  };
4342
4458
  }
4343
4459
  async function getLatestAddedTargets(client, config, input) {
@@ -4593,10 +4709,12 @@ async function fetchProgramTargetsWithEndpointFallback(client, programId) {
4593
4709
  requestId: api.requestId,
4594
4710
  upstreamRequestId: api.upstreamRequestId,
4595
4711
  fetchedAt: api.fetchedAt,
4712
+ noStore: api.noStore,
4596
4713
  cache: readObject(stripUndefined({
4597
4714
  hit: api.cached,
4598
4715
  coalesced_live_request: api.coalesced,
4599
- expires_at: api.cacheExpiresAt
4716
+ expires_at: api.cacheExpiresAt,
4717
+ no_store: api.noStore
4600
4718
  })),
4601
4719
  source: "program_targets",
4602
4720
  rawTargets,
@@ -4654,7 +4772,7 @@ function getCachedProgramTargetsFallback(client, programId) {
4654
4772
  }
4655
4773
  function rememberProgramTargetsFallback(client, programId, lookup) {
4656
4774
  const diagnostics = client.diagnostics();
4657
- if (!diagnostics.response_cache.enabled || lookup.unavailable) {
4775
+ if (!diagnostics.response_cache.enabled || lookup.unavailable || lookup.noStore) {
4658
4776
  return;
4659
4777
  }
4660
4778
  const cache = programTargetsFallbackCacheForClient(client);
@@ -4692,7 +4810,7 @@ function pruneProgramTargetsFallbackCache(cache) {
4692
4810
  }
4693
4811
  }
4694
4812
  function programTargetsFallbackCacheKey(programId) {
4695
- return normalizeTag(programId);
4813
+ return programId;
4696
4814
  }
4697
4815
  function cacheTtlMsFromDiagnostics(cache) {
4698
4816
  return cache.enabled ? cache.ttl_ms : 0;
@@ -4711,10 +4829,12 @@ async function fetchProgramTargetsFromExportFallback(client, programId) {
4711
4829
  requestId: api.requestId,
4712
4830
  upstreamRequestId: api.upstreamRequestId,
4713
4831
  fetchedAt: api.fetchedAt,
4832
+ noStore: api.noStore,
4714
4833
  cache: readObject(stripUndefined({
4715
4834
  hit: api.cached,
4716
4835
  coalesced_live_request: api.coalesced,
4717
- expires_at: api.cacheExpiresAt
4836
+ expires_at: api.cacheExpiresAt,
4837
+ no_store: api.noStore
4718
4838
  })),
4719
4839
  source: "target_export",
4720
4840
  rawTargets,
@@ -4761,7 +4881,8 @@ async function getProgramFromTargetsOnlyFallback(client, programId, programError
4761
4881
  cache: readObject(stripUndefined({
4762
4882
  hit: targetsApi.cached,
4763
4883
  coalesced_live_request: targetsApi.coalesced,
4764
- expires_at: targetsApi.cacheExpiresAt
4884
+ expires_at: targetsApi.cacheExpiresAt,
4885
+ no_store: targetsApi.noStore
4765
4886
  })),
4766
4887
  source_requests: [
4767
4888
  stripUndefined({
@@ -4906,8 +5027,6 @@ async function exportTargetsFromProgramTargetsFallback(client, config, exportSto
4906
5027
  generated_at: new Date().toISOString()
4907
5028
  });
4908
5029
  const exportId = randomUUID();
4909
- const resourceUri = exportResourceUri(exportId);
4910
- rememberExport(exportStore, exportId, exportPayload);
4911
5030
  return stripUndefined({
4912
5031
  request_id: randomUUID(),
4913
5032
  source_requests: sourceRequests,
@@ -4915,7 +5034,7 @@ async function exportTargetsFromProgramTargetsFallback(client, config, exportSto
4915
5034
  errors: errors.length > 0 ? errors : undefined,
4916
5035
  export: {
4917
5036
  export_id: exportId,
4918
- resource_uri: resourceUri,
5037
+ data: exportPayload,
4919
5038
  preview: previewExportPayload(exportPayload),
4920
5039
  returned_preview_count: previewExportCount(exportPayload),
4921
5040
  limit: input.limit
@@ -5642,6 +5761,10 @@ function applyCollectionContinuation(payload, field, label, store, config) {
5642
5761
  if (!items || items.length <= inlineLimit) {
5643
5762
  return payload;
5644
5763
  }
5764
+ // A no-store page stays only in this tool result, never in a reusable resource.
5765
+ if (readBoolean(readObject(payload.cache)?.no_store) === true) {
5766
+ return payload;
5767
+ }
5645
5768
  const continuationId = randomUUID();
5646
5769
  const expiresAt = Date.now() + (config.continuationTtlMs ?? LOCAL_EXPORT_TTL_MS);
5647
5770
  const stored = rememberContinuation(store, continuationId, label, items, expiresAt);
@@ -5898,10 +6021,12 @@ function formatProgram(program, mode) {
5898
6021
  function compactProgram(program) {
5899
6022
  return stripUndefined({
5900
6023
  id: stringField(program, "id"),
6024
+ catalog_id: stringField(program, "catalog_id"),
5901
6025
  handle: stringField(program, "handle"),
5902
6026
  name: stringField(program, "name"),
5903
6027
  platform: stringField(program, "platform"),
5904
6028
  catalog: stringField(program, "catalog"),
6029
+ program_kind: stringField(program, "program_kind"),
5905
6030
  program_status: stringField(program, "program_status"),
5906
6031
  is_active: readBoolean(program.is_active),
5907
6032
  program_type: stringField(program, "program_type"),
@@ -5910,15 +6035,21 @@ function compactProgram(program) {
5910
6035
  policy_url: stringField(program, "policy_url"),
5911
6036
  reporting_url: stringField(program, "reporting_url"),
5912
6037
  reward_status: stringField(program, "reward_status"),
5913
- data_capabilities: readObject(program.data_capabilities),
5914
- opportunity_tag: readObject(program.opportunity_tag),
6038
+ data_capabilities: program.data_capabilities,
6039
+ opportunity_tag: program.opportunity_tag,
5915
6040
  competition_risk: stringField(program, "competition_risk"),
5916
6041
  competition_risk_reason: stringField(program, "competition_risk_reason"),
5917
- submission_cost: readObject(program.submission_cost),
5918
- reward_range: readObject(program.reward_range),
5919
- public_report_count: readNumber(program.public_report_count),
6042
+ submission_cost: program.submission_cost,
6043
+ reward_range: program.reward_range,
6044
+ reward_range_label: stringField(program, "reward_range_label"),
6045
+ public_report_count: program.public_report_count,
5920
6046
  public_report_count_label: stringField(program, "public_report_count_label"),
5921
6047
  first_seen_date: stringField(program, "first_seen_date"),
6048
+ launch_at: program.launch_at,
6049
+ launch_at_precision: stringField(program, "launch_at_precision"),
6050
+ ends_at: program.ends_at,
6051
+ ends_on: program.ends_on,
6052
+ verification: program.verification,
5922
6053
  last_updated: stringField(program, "last_updated"),
5923
6054
  targets_updated_at: stringField(program, "targets_updated_at"),
5924
6055
  scope_summary: readObject(program.scope_summary),
@@ -6240,7 +6371,7 @@ function candidateLooksLikeCtf(candidate) {
6240
6371
  return candidate.looksLikeCtf;
6241
6372
  }
6242
6373
  function candidateLooksContestLike(candidate) {
6243
- return tagIncludesAnyNormalized(candidate.normalizedSearchSignals, WEB3_CONTEST_SIGNALS);
6374
+ return stringField(candidate.program, "program_kind") === "contest";
6244
6375
  }
6245
6376
  function candidateHasKnownNoBounty(candidate) {
6246
6377
  const programType = normalizeTag(stringField(candidate.program, "program_type") ?? "");
@@ -7346,6 +7477,7 @@ async function findPrograms(client, config, input) {
7346
7477
  const collected = await collectPrograms(client, {
7347
7478
  platforms: input.platforms,
7348
7479
  tags: upstreamTags,
7480
+ program_kind: input.program_kind ?? (input.contest_like ? "contest" : undefined),
7349
7481
  updated_since: input.updated_since,
7350
7482
  opportunity_levels: input.opportunity_levels,
7351
7483
  max_pages: input.max_pages,
@@ -7394,6 +7526,7 @@ async function findPrograms(client, config, input) {
7394
7526
  exclude_non_web: input.exclude_non_web,
7395
7527
  has_api_scope: input.has_api_scope,
7396
7528
  contest_like: input.contest_like ? true : undefined,
7529
+ program_kind: input.program_kind,
7397
7530
  vdp_mode: input.vdp_mode && input.vdp_mode !== "include" ? input.vdp_mode : undefined,
7398
7531
  fresh_launch_days: input.fresh_launch_days,
7399
7532
  min_wildcards: input.min_wildcards,
@@ -7823,6 +7956,7 @@ async function fetchProgramPage(client, source, sourceIndex, page, options) {
7823
7956
  platforms: options.platforms,
7824
7957
  tags: options.tags,
7825
7958
  updated_since: options.updated_since,
7959
+ program_kind: source.kind === "programs" ? options.program_kind : undefined,
7826
7960
  page,
7827
7961
  page_size: MAX_PROGRAMS_PER_SEARCH
7828
7962
  });
@@ -7908,6 +8042,9 @@ function toProgramCandidate(rawProgram, webBaseUrl) {
7908
8042
  return candidate;
7909
8043
  }
7910
8044
  function programCandidateMatches(candidate, filters) {
8045
+ if (filters.program_kind && stringField(candidate.program, "program_kind") !== filters.program_kind) {
8046
+ return false;
8047
+ }
7911
8048
  const programStatus = normalizeTag(stringField(candidate.program, "program_status") ?? "");
7912
8049
  if (readBoolean(candidate.program.is_active) === false || programStatus === "paused") {
7913
8050
  return false;
@@ -8270,7 +8407,7 @@ function registerPrompts(server, profile) {
8270
8407
  tags: z.string().optional().describe("Optional comma-separated scope/language/target-type tags."),
8271
8408
  max_programs: z.string().optional().describe("Optional maximum shortlist size.")
8272
8409
  }
8273
- }, (args) => promptResult(`Use the BBRadar MCP tools only. Do not invoke local bug bounty skills, methodology files, worklogs, or non-BBRadar tools unless the user explicitly asks for methodology. Do not perform active scanning or contact third-party targets. Treat results as fresh API reads; do not describe them as cached.
8410
+ }, (args) => promptResult(`Use the BBRadar MCP tools only. Do not invoke local bug bounty skills, methodology files, worklogs, or non-BBRadar tools unless the user explicitly asks for methodology. Do not perform active scanning or contact third-party targets. Check each result's cache metadata before describing its freshness.
8274
8411
 
8275
8412
  Find the best BBRadar program candidates.
8276
8413
  ${discoveryToolInstructions}
@@ -8321,8 +8458,12 @@ The plan must:
8321
8458
  - Prioritize targets by bounty eligibility, freshness, wildcard status, target type, and severity labels.
8322
8459
  - Include a short checklist of BBRadar follow-up tool calls and decision points.`));
8323
8460
  }
8324
- function resolveFeedCursor(store, endpoint, filters, resume) {
8325
- const normalizedFilters = stableStringify(normalizeFeedCursorFilters(filters));
8461
+ function resolveFeedCursor(store, endpoint, filters, resume, config) {
8462
+ const normalizedFilters = stableStringify({
8463
+ api_base_url: config.apiBaseUrl,
8464
+ credential_scope: credentialCacheScope(config),
8465
+ filters: normalizeFeedCursorFilters(filters)
8466
+ });
8326
8467
  const key = `${endpoint}:${createHash("sha256").update(normalizedFilters).digest("hex")}`;
8327
8468
  const explicitAfterId = readNumber(filters.after_id);
8328
8469
  if (explicitAfterId !== undefined) {
@@ -8353,7 +8494,7 @@ function advanceFeedCursor(store, cursor, rawMeta) {
8353
8494
  stored_in_memory: true
8354
8495
  };
8355
8496
  }
8356
- async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudgetMs, initialSnapshot, fetchPage, refreshInventory) {
8497
+ async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudgetMs, initialSnapshot, initialPage, fetchPage, refreshInventory) {
8357
8498
  const startedAt = Date.now();
8358
8499
  const results = [];
8359
8500
  const sourceRequests = [];
@@ -8370,7 +8511,7 @@ async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudget
8370
8511
  do {
8371
8512
  let api;
8372
8513
  try {
8373
- api = await fetchPage(currentCursor.afterId, undefined, activeSnapshot);
8514
+ api = await fetchPage(currentCursor.afterId, undefined, activeSnapshot, activeSnapshot ? initialPage + pagesFetched : 1);
8374
8515
  feedRecoveryProgress.get(store)?.delete(currentCursor.key);
8375
8516
  }
8376
8517
  catch (error) {
@@ -8466,6 +8607,10 @@ async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudget
8466
8607
  }
8467
8608
  const data = readObject(api.data);
8468
8609
  const returnedSnapshot = readString(readObject(data?.meta)?.snapshot);
8610
+ if (activeSnapshot && !returnedSnapshot)
8611
+ throw new Error("Stable snapshot response omitted its token; discard this page and start a new snapshot.");
8612
+ if (activeSnapshot && activeSnapshot !== "start" && returnedSnapshot !== activeSnapshot)
8613
+ throw new Error("Stable snapshot changed between pages; discard these pages and start a new snapshot.");
8469
8614
  if (activeSnapshot === "start" && returnedSnapshot) {
8470
8615
  activeSnapshot = returnedSnapshot;
8471
8616
  }
@@ -8668,21 +8813,49 @@ function withApiMetadata(api, payload) {
8668
8813
  hit: api.cached,
8669
8814
  stale_while_revalidate: api.stale,
8670
8815
  coalesced_live_request: api.coalesced,
8671
- expires_at: api.cacheExpiresAt
8816
+ expires_at: api.cacheExpiresAt,
8817
+ no_store: api.noStore
8672
8818
  }),
8673
8819
  fetched_at: api.fetchedAt,
8674
8820
  ...payload
8675
8821
  });
8676
8822
  }
8823
+ function proPage(api, field) {
8824
+ const raw = readObject(api.data);
8825
+ if (!raw || !(field === "activity_summary" ? readObject(raw[field]) : Array.isArray(raw[field]))) {
8826
+ throw new Error(`BBRadar Pro API returned a malformed page: expected ${field}.`);
8827
+ }
8828
+ const data = sanitizeProPayload(api.data);
8829
+ return { ...data, ...withApiMetadata(api, {}) };
8830
+ }
8831
+ function assertProFeature(client, endpoint, flag) {
8832
+ const state = client.capabilitySnapshot();
8833
+ if (state.status !== "negotiated")
8834
+ return;
8835
+ const capability = client.endpointCapability(endpoint);
8836
+ if (!capability)
8837
+ throw new Error(`The negotiated BBRadar API does not advertise ${endpoint}. Upgrade the API deployment to use this tool.`);
8838
+ if (flag && capability[flag] === false) {
8839
+ throw new Error(`${endpoint} is advertised but ${flag}=false on this API deployment.`);
8840
+ }
8841
+ }
8842
+ function assertHistoryCapture(client) {
8843
+ if (client.capabilitySnapshot().status === "negotiated" &&
8844
+ client.endpointCapability("history_changes")?.history_capture_enabled === false) {
8845
+ throw new Error("History endpoints are advertised but history_capture_enabled=false on this API deployment.");
8846
+ }
8847
+ }
8677
8848
  function apiSourceMetadata(api) {
8678
8849
  return stripUndefined({
8679
8850
  cache_hit: api.cached,
8680
8851
  cache_expires_at: api.cacheExpiresAt,
8681
- coalesced_live_request: api.coalesced
8852
+ coalesced_live_request: api.coalesced,
8853
+ no_store: api.noStore
8682
8854
  });
8683
8855
  }
8684
8856
  function errorPayload(error, decision, toolName, durationMs) {
8685
8857
  if (error instanceof BBRadarApiError) {
8858
+ const recovery = error.status === 410 ? recoveryForGone(error, toolName) : undefined;
8686
8859
  return stripUndefined({
8687
8860
  request_id: error.requestId,
8688
8861
  upstream_request_id: error.upstreamRequestId,
@@ -8692,6 +8865,7 @@ function errorPayload(error, decision, toolName, durationMs) {
8692
8865
  detail: sanitizeJson(error.detail),
8693
8866
  errors: sanitizeJson(error.errors)
8694
8867
  }),
8868
+ recovery,
8695
8869
  mcp_rate_limit: toRateLimitPayload(decision),
8696
8870
  mcp_timing: toolTimingPayload(toolName, durationMs)
8697
8871
  });
@@ -8705,6 +8879,19 @@ function errorPayload(error, decision, toolName, durationMs) {
8705
8879
  mcp_timing: toolTimingPayload(toolName, durationMs)
8706
8880
  };
8707
8881
  }
8882
+ function recoveryForGone(error, toolName) {
8883
+ const detail = typeof error.detail === "string" ? error.detail.toLowerCase() : error.message.toLowerCase();
8884
+ if (detail.includes("retained analytics") || detail.includes("precede")) {
8885
+ return { action: "choose_supported_interval", message: "Choose dates inside the retained analytics window." };
8886
+ }
8887
+ if (detail.includes("snapshot")) {
8888
+ return { action: "start_new_snapshot", message: "Restart on page 1 with snapshot=start; do not combine pages from the old snapshot." };
8889
+ }
8890
+ if (toolName.startsWith("get_history") || toolName === "get_program_history" || toolName.startsWith("get_analytics")) {
8891
+ return { action: "restart_query", message: "Restart this History or analytics query without its expired opaque cursor, keeping the intended filters." };
8892
+ }
8893
+ return { action: "reconcile_inventory", message: "The retained numeric feed has a gap; reconcile current inventory before seeding a new after_id." };
8894
+ }
8708
8895
  function recordToolMetric(toolName, durationMs, status) {
8709
8896
  const existing = TOOL_METRICS.get(toolName) ??
8710
8897
  {
@@ -8823,15 +9010,19 @@ function compactTarget(target) {
8823
9010
  identifier: targetIdentifier(target),
8824
9011
  target_type: stringField(target, "target_type"),
8825
9012
  in_scope: readBoolean(target.in_scope),
8826
- eligible_for_bounty: readBoolean(target.eligible_for_bounty),
9013
+ eligible_for_bounty: target.eligible_for_bounty,
8827
9014
  wildcard: readBoolean(target.wildcard),
8828
9015
  path_kind: stringField(target, "path_kind"),
8829
9016
  path_prefix: stringField(target, "path_prefix"),
8830
9017
  match_quality: stringField(target, "match_quality"),
8831
9018
  match_reason: stringField(target, "match_reason"),
8832
- target_opportunity_score: readNumber(target.target_opportunity_score),
9019
+ target_opportunity_score: target.target_opportunity_score,
8833
9020
  target_opportunity_label: stringField(target, "target_opportunity_label"),
9021
+ score_version: target.score_version,
9022
+ evidence_coverage: target.evidence_coverage,
9023
+ target_score_breakdown: target.target_score_breakdown,
8834
9024
  competition_risk: stringField(target, "competition_risk"),
9025
+ competition_risk_reason: stringField(target, "competition_risk_reason"),
8835
9026
  why_this_target: stringField(target, "why_this_target")
8836
9027
  });
8837
9028
  }