@bbradar/mcp 0.2.0 → 0.2.1

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
@@ -131,7 +131,7 @@ Large target and change collections return at most 50 inline records by default.
131
131
 
132
132
  ## Performance Tuning
133
133
 
134
- The defaults expose the 16-tool core surface and compact text results while enabling bounded concurrency, positive response caching, in-flight request coalescing, stale-while-revalidate program metadata, endpoint circuit breakers, and short negative caching for repeated 404/405 lookups. Set the tool profile to `full` when all specialized tools are needed.
134
+ The defaults expose the 17-tool core surface and compact text results while enabling bounded concurrency, positive response caching, in-flight request coalescing, stale-while-revalidate program metadata, endpoint circuit breakers, and short negative caching for repeated 404/405 lookups. Set the tool profile to `full` when all specialized tools are needed.
135
135
 
136
136
  | Environment variable | Default | Purpose |
137
137
  | --- | ---: | --- |
@@ -141,7 +141,7 @@ The defaults expose the 16-tool core surface and compact text results while enab
141
141
  | `BBRADAR_MCP_MAX_CONCURRENT_REQUESTS` | `6` | Maximum simultaneous upstream BBRadar API requests. |
142
142
  | `BBRADAR_MCP_REQUEST_TIMEOUT_SECONDS` | `30` | Timeout for each active upstream request; queue time is excluded. |
143
143
  | `BBRADAR_MCP_COMPACT_TOOL_TEXT` | `true` | Avoid duplicating full results for clients that consume MCP `structuredContent`. Set to `false` for legacy text-only clients. |
144
- | `BBRADAR_MCP_TOOL_PROFILE` | `core` | Compact 16-tool discovery surface. Set to `full` for every specialized tool. |
144
+ | `BBRADAR_MCP_TOOL_PROFILE` | `core` | Compact 17-tool discovery surface. Set to `full` for every specialized tool. |
145
145
  | `BBRADAR_MCP_PROGRAM_STALE_WHILE_REVALIDATE_SECONDS` | `300` | Maximum stale window for exact program metadata while one background refresh runs. |
146
146
  | `BBRADAR_MCP_CIRCUIT_BREAKER_FAILURE_THRESHOLD` | `3` | Consecutive retryable endpoint failures before its circuit opens. |
147
147
  | `BBRADAR_MCP_CIRCUIT_BREAKER_COOLDOWN_SECONDS` | `30` | Endpoint circuit cooldown before a new request is admitted. |
@@ -164,7 +164,7 @@ Run `npm run benchmark` for the deterministic local performance benchmark. It re
164
164
 
165
165
  Use `find_programs` as the generic ranked path for combinations of platform, reward, opportunity, scope breadth, technology, competition, and freshness signals. It supports both inclusion and exclusion through `platforms`, `exclude_platforms`, `web3`, `exclude_web3`, and the remaining scope/reward/activity filters. When a user describes an exclusion without naming exact platform values, call `list_filters` first and reuse the observed values. If an intentionally strict combination returns no candidates, relax one local threshold at a time; the upstream program pages are cached, so these fallback passes normally add no API requests.
166
166
 
167
- For repository-aware hunting, call `get_repository_changes` and `find_programs` in parallel with the same requested platform set. The repository feed supplies recent events and scored targets while `find_programs` supplies reward and ranking data. Intersect their program ids, discard non-requested programs that merely share the same repository, and treat targets as event-level associations when the feed omits `program_id`. This remains generic across platforms and technology stacks and avoids a specialized tool for every prompt variation.
167
+ For repository-aware hunting, use `find_repository_hunt_targets`. It fetches repository events and program/reward candidates concurrently inside one MCP call, joins them by program id, filters scored targets, removes non-requested associated platforms, and ranks the final matches by a balanced, reward-first, target-first, or recency-first strategy. The inputs remain generic across platforms and technology stacks, so the AI client does not need to plan and wait for a long sequence of separate calls.
168
168
 
169
169
  ### Optimized wildcard discovery
170
170
 
@@ -192,6 +192,7 @@ This path uses one recent-changes request. Use `find_wildcard_programs` for a ra
192
192
  - `get_program_targets`
193
193
  - `get_recent_changes`
194
194
  - `get_repository_changes`
195
+ - `find_repository_hunt_targets`
195
196
  - `get_opportunities`
196
197
  - `find_programs`
197
198
  - `find_wildcard_programs`
package/dist/server.js CHANGED
@@ -6,7 +6,7 @@ import { NumericCursorStore } from "./cursorStore.js";
6
6
  import { stableStringify, stripUndefined } from "./json.js";
7
7
  import { FixedWindowRateLimiter } from "./rateLimit.js";
8
8
  import { readArray, readBoolean, readNumber, readObject, readString, sanitizeJson, sanitizeChange, sanitizeProgram, sanitizeRepositoryChange, sanitizeTarget, sanitizeTargetsForExport } from "./sanitize.js";
9
- const SERVER_VERSION = "0.2.0";
9
+ const SERVER_VERSION = "0.2.1";
10
10
  const MAX_PROGRAMS_PER_SEARCH = 100;
11
11
  const MAX_TARGET_EXPORT = 500;
12
12
  const MAX_RECENT_CHANGES = 100;
@@ -50,6 +50,7 @@ const CORE_TOOL_NAMES = new Set([
50
50
  "get_program_targets",
51
51
  "get_recent_changes",
52
52
  "get_repository_changes",
53
+ "find_repository_hunt_targets",
53
54
  "get_latest_added_targets",
54
55
  "get_opportunities",
55
56
  "find_programs",
@@ -61,6 +62,7 @@ const CORE_TOOL_NAMES = new Set([
61
62
  ]);
62
63
  const TOOL_ENDPOINT_REQUIREMENTS = {
63
64
  get_repository_changes: "repository_changes",
65
+ find_repository_hunt_targets: "repository_changes",
64
66
  export_targets: "target_export",
65
67
  get_opportunities: "opportunities"
66
68
  };
@@ -682,7 +684,7 @@ export function createBbradarServer(client, config) {
682
684
  }));
683
685
  registerTool("get_repository_changes", {
684
686
  title: "Get Repository Changes",
685
- description: "Read-only repository push/change feed. Defaults to lightweight program summaries; use after_id for efficient incremental polling. For rewarded Web3 repository targets, call this in parallel with find_programs using the same platforms, then intersect event program ids with the ranked rewarded programs.",
687
+ description: "Read-only repository push/change feed for direct inspection or polling. Defaults to lightweight program summaries; use after_id for efficient incremental polling. Use find_repository_hunt_targets instead when reward or target-quality ranking is also required.",
686
688
  inputSchema: {
687
689
  after_id: z.number().int().min(0).optional().describe("Incremental cursor; returns events after this id in ascending order."),
688
690
  resume_cursor: z.boolean().default(false).describe("Resume the in-memory cursor for this exact normalized filter set. Seed it once with after_id."),
@@ -739,6 +741,51 @@ export function createBbradarServer(client, config) {
739
741
  source_requests: collected.sourceRequests.length > 1 ? collected.sourceRequests : undefined
740
742
  }), "repository_changes", "repository changes", continuationStore, config);
741
743
  }));
744
+ registerTool("find_repository_hunt_targets", {
745
+ title: "Find Repository-Aware Hunt Targets",
746
+ description: "Fast one-call path for requests that combine recent repository activity with program rewards and target quality. Fetches program rankings and repository changes in parallel, joins them locally, and works with arbitrary platforms, Web2/Web3 modes, reward thresholds, target types, languages, and freshness criteria.",
747
+ inputSchema: {
748
+ platforms: platformListSchema(),
749
+ exclude_platforms: stringListSchema("Platforms to exclude from final matches."),
750
+ tags: tagListSchema(),
751
+ scope_tags: scopeTagListSchema(),
752
+ target_types: targetTypeListSchema(),
753
+ language_tags: languageTagListSchema(),
754
+ surface_mode: z.enum(["any", "web2", "web3"]).default("any"),
755
+ opportunity_levels: z.array(opportunityLevelSchema).max(4).default([]),
756
+ min_bounty_max: z.number().int().min(0).optional(),
757
+ require_bounty: z.boolean().default(true),
758
+ max_public_report_count: z.number().int().min(0).optional(),
759
+ include_unknown_report_count: z.boolean().default(true),
760
+ min_eligible_targets: z.number().int().min(0).default(1),
761
+ min_total_targets: z.number().int().min(0).optional(),
762
+ min_wildcards: z.number().int().min(0).max(1000).default(0),
763
+ min_added_7d: z.number().int().min(0).optional(),
764
+ changed_since: isoDateTimeSchema.optional(),
765
+ free_submission_only: z.boolean().default(false),
766
+ min_target_score: z.number().int().min(0).max(100).default(60),
767
+ target_opportunity_labels: z.array(targetOpportunityLabelSchema).max(4).default([]),
768
+ competition_risks: z.array(competitionRiskSchema).max(4).default([]),
769
+ max_program_pages: z.number().int().min(1).max(MAX_FIND_PROGRAM_PAGES).default(3),
770
+ max_program_candidates: z.number().int().min(1).max(MAX_FIND_PROGRAM_RESULTS).default(MAX_FIND_PROGRAM_RESULTS),
771
+ repository_page_size: z.number().int().min(1).max(maxRepositoryChangesPageSize).default(Math.min(100, maxRepositoryChangesPageSize)),
772
+ max_results: z.number().int().min(1).max(25).default(10),
773
+ max_targets_per_result: z.number().int().min(1).max(20).default(5),
774
+ ranking_mode: z.enum(["balanced", "highest_reward", "best_target", "newest"]).default("balanced"),
775
+ output_mode: repositoryChangeListModeSchema.default("compact"),
776
+ upstream_request_budget: z.number().int().min(2).max(MAX_FIND_UPSTREAM_REQUEST_BUDGET).default(10)
777
+ },
778
+ outputSchema: {
779
+ ...apiEnvelopeOutputShape(),
780
+ matches: z.array(jsonRecordSchema()).optional(),
781
+ programs: z.array(jsonRecordSchema()).optional(),
782
+ filters: jsonRecordSchema().optional(),
783
+ upstream_budget: jsonRecordSchema().optional(),
784
+ ranking_scope: jsonRecordSchema().optional(),
785
+ meta: jsonRecordSchema().optional()
786
+ },
787
+ annotations: readOnlyAnnotations
788
+ }, (args) => runTool("find_repository_hunt_targets", config, rateLimiter, async () => findRepositoryHuntTargets(client, config, args)));
742
789
  registerTool("get_latest_added_targets", {
743
790
  title: "Get Latest Added Targets",
744
791
  description: "Latest added targets by type with optional platform, reward, and report-count filters. For fast wildcard discovery use target_type=wildcard and include_full_target_list=false.",
@@ -1839,6 +1886,12 @@ function bbradarGuide(topic, profile, client) {
1839
1886
  minimum_arguments: {},
1840
1887
  useful_arguments: ["platforms", "target_types", "language_tags", "min_bounty_max", "max_public_report_count", "fresh_launch_days", "has_api_scope"]
1841
1888
  },
1889
+ {
1890
+ tool: "find_repository_hunt_targets",
1891
+ use_when: "The request combines repository activity with reward, target-quality, platform, technology, or freshness filters.",
1892
+ minimum_arguments: {},
1893
+ useful_arguments: ["platforms", "exclude_platforms", "surface_mode", "min_bounty_max", "min_target_score", "changed_since", "ranking_mode"]
1894
+ },
1842
1895
  {
1843
1896
  tool: "run_program_name_action",
1844
1897
  use_when: "The user names one program and asks for a brief, targets, new targets, scope delta, or target breakdown.",
@@ -1895,14 +1948,10 @@ function bbradarGuide(topic, profile, client) {
1895
1948
  then: "For incremental polling, seed once with after_id and later set resume_cursor=true with the same filters."
1896
1949
  },
1897
1950
  {
1898
- user_goal: "rewarded Web3 targets with recent repository changes on named platforms",
1899
- first_tool: "get_repository_changes",
1900
- arguments: { platforms: ["requested platforms"], include_targets: true, output_mode: "full", page_size: 100 },
1901
- parallel_with: {
1902
- tool: "find_programs",
1903
- arguments: { platforms: ["requested platforms"], web3: true, require_bounty: true, min_bounty_max: 5000, sort_by: "highest_reward", include_target_samples: false }
1904
- },
1905
- then: "Intersect repository-event program ids with ranked program ids. Targets belong to the repository event association even when target rows omit program_id; keep strong target scores/labels and only the requested program platforms."
1951
+ user_goal: "hunt targets combining repository activity with rewards, target quality, platforms, or Web2/Web3 constraints",
1952
+ first_tool: "find_repository_hunt_targets",
1953
+ arguments: { platforms: ["requested platforms"], surface_mode: "web3", min_bounty_max: 5000, min_target_score: 60 },
1954
+ then: "Use this single composite call instead of manually chaining program discovery and repository tools. Omit or change any example filter the user did not request."
1906
1955
  },
1907
1956
  {
1908
1957
  user_goal: "latest added wildcard targets with platform, reward, or report-count filters",
@@ -2019,16 +2068,6 @@ function coreBbradarGuide(topic, client) {
2019
2068
  first_tool: "find_programs",
2020
2069
  arguments: { max_results: 10, output_mode: "compact" }
2021
2070
  },
2022
- {
2023
- user_goal: "Web2 programs with broad scope, good rewards, recent target activity, and excluded platforms",
2024
- first_tool: "list_filters",
2025
- then: "Resolve excluded platform names, then call find_programs with exclude_platforms, exclude_web3=true, exclude_non_web=true, reward/scope/freshness thresholds, and sort_by=most_new_targets. Relax one local threshold on an empty result; cached source pages make retries fast."
2026
- },
2027
- {
2028
- user_goal: "named program summary or action",
2029
- first_tool: "run_program_name_action",
2030
- arguments: { query: "program name", action: "brief" }
2031
- },
2032
2071
  {
2033
2072
  user_goal: "named program targets",
2034
2073
  first_tool: "get_program_eligible_targets_by_name",
@@ -2050,14 +2089,10 @@ function coreBbradarGuide(topic, client) {
2050
2089
  arguments: { include_targets: false, max_cursor_pages: 2 }
2051
2090
  },
2052
2091
  {
2053
- user_goal: "rewarded Web3 targets with recent repository changes on named platforms",
2054
- first_tool: "get_repository_changes",
2055
- arguments: { platforms: ["requested platforms"], include_targets: true, output_mode: "full", page_size: 100 },
2056
- parallel_with: {
2057
- tool: "find_programs",
2058
- arguments: { platforms: ["requested platforms"], web3: true, require_bounty: true, min_bounty_max: 5000, sort_by: "highest_reward", include_target_samples: false }
2059
- },
2060
- then: "Intersect event program ids with ranked program ids. Treat targets as event-level associations when target rows omit program_id, keep strong target scores/labels, and discard non-requested associated platforms."
2092
+ user_goal: "hunt targets combining repository activity with rewards, target quality, platforms, or Web2/Web3 constraints",
2093
+ first_tool: "find_repository_hunt_targets",
2094
+ arguments: { platforms: ["requested platforms"], surface_mode: "web3", min_bounty_max: 5000, min_target_score: 60 },
2095
+ then: "Use this one call instead of manually chaining program and repository tools. Omit or change any example filter the user did not request."
2061
2096
  },
2062
2097
  {
2063
2098
  user_goal: "export targets",
@@ -2084,6 +2119,7 @@ function coreBbradarGuide(topic, client) {
2084
2119
  },
2085
2120
  primary_entry_points: [
2086
2121
  "find_programs",
2122
+ "find_repository_hunt_targets",
2087
2123
  "run_program_name_action",
2088
2124
  "get_program_eligible_targets_by_name",
2089
2125
  "find_programs_by_target",
@@ -6258,6 +6294,285 @@ function clampLimit(name, value, max, warnings) {
6258
6294
  warnings.push(`${name} was clamped from ${value} to ${max}.`);
6259
6295
  return max;
6260
6296
  }
6297
+ async function findRepositoryHuntTargets(client, config, input) {
6298
+ const warnings = [];
6299
+ const repositoryRequestReserve = input.upstream_request_budget >= 3 ? 2 : 1;
6300
+ const programRequestBudget = Math.max(1, input.upstream_request_budget - repositoryRequestReserve);
6301
+ const programInput = {
6302
+ platforms: input.platforms,
6303
+ exclude_platforms: input.exclude_platforms,
6304
+ tags: input.tags,
6305
+ scope_tags: input.scope_tags,
6306
+ target_types: input.target_types,
6307
+ language_tags: input.language_tags,
6308
+ web3: input.surface_mode === "web3",
6309
+ exclude_web3: input.surface_mode === "web2",
6310
+ opportunity_levels: input.opportunity_levels,
6311
+ updated_since: undefined,
6312
+ min_bounty_min: undefined,
6313
+ min_bounty_max: input.min_bounty_max,
6314
+ max_bounty_max: undefined,
6315
+ require_bounty: input.require_bounty,
6316
+ max_public_report_count: input.max_public_report_count,
6317
+ require_known_report_count: false,
6318
+ include_unknown_report_count: input.include_unknown_report_count,
6319
+ exclude_ctf: true,
6320
+ exclude_mobile_only: false,
6321
+ exclude_non_web: input.surface_mode === "web2",
6322
+ has_api_scope: false,
6323
+ contest_like: false,
6324
+ vdp_mode: "exclude",
6325
+ fresh_launch_days: undefined,
6326
+ min_wildcards: input.min_wildcards,
6327
+ min_eligible_targets: input.min_eligible_targets,
6328
+ min_total_targets: input.min_total_targets,
6329
+ min_added_24h: undefined,
6330
+ min_added_7d: input.min_added_7d,
6331
+ max_pages: input.max_program_pages,
6332
+ max_results: input.max_program_candidates,
6333
+ sort_by: "highest_reward",
6334
+ include_target_samples: false,
6335
+ target_sample_programs: 0,
6336
+ target_sample_size: 1,
6337
+ only_in_scope_targets: true,
6338
+ only_bounty_eligible_targets: true,
6339
+ target_samples_only_wildcards: false,
6340
+ output_mode: "compact",
6341
+ upstream_request_budget: programRequestBudget
6342
+ };
6343
+ const [programPayload, repository] = await Promise.all([
6344
+ findPrograms(client, config, programInput),
6345
+ fetchRepositoryHuntPage(client, input, warnings, repositoryRequestReserve)
6346
+ ]);
6347
+ const candidatePrograms = readArray(programPayload.programs)
6348
+ .map(readObject)
6349
+ .filter((program) => program !== undefined);
6350
+ const candidatesById = new Map(candidatePrograms
6351
+ .map((program) => [stringField(program, "id"), program])
6352
+ .filter((entry) => entry[0] !== undefined));
6353
+ const changedSinceTime = input.changed_since ? Date.parse(input.changed_since) : undefined;
6354
+ const rankedMatches = [];
6355
+ let qualifyingTargetCount = 0;
6356
+ for (const rawChange of readArray(readObject(repository.api.data)?.results)) {
6357
+ const change = sanitizeRepositoryChange(rawChange, config.webBaseUrl);
6358
+ const observedAtText = stringField(change, "observed_at");
6359
+ const observedAt = observedAtText ? Date.parse(observedAtText) : 0;
6360
+ if (changedSinceTime !== undefined && (!Number.isFinite(observedAt) || observedAt < changedSinceTime)) {
6361
+ continue;
6362
+ }
6363
+ const programs = readArray(change.programs)
6364
+ .map((program) => stringField(readObject(program), "id"))
6365
+ .filter((id) => id !== undefined)
6366
+ .map((id) => candidatesById.get(id))
6367
+ .filter((program) => program !== undefined);
6368
+ if (programs.length === 0) {
6369
+ continue;
6370
+ }
6371
+ const targets = readArray(change.targets)
6372
+ .map(readObject)
6373
+ .filter((target) => target !== undefined)
6374
+ .filter((target) => repositoryHuntTargetMatches(target, input))
6375
+ .sort((left, right) => (readNumber(right.target_opportunity_score) ?? 0) - (readNumber(left.target_opportunity_score) ?? 0));
6376
+ if (targets.length === 0) {
6377
+ continue;
6378
+ }
6379
+ qualifyingTargetCount += targets.length;
6380
+ const maxReward = Math.max(0, ...programs.map(programMaximumReward));
6381
+ const maxTargetScore = Math.max(0, ...targets.map((target) => readNumber(target.target_opportunity_score) ?? 0));
6382
+ const balancedScore = repositoryHuntBalancedScore(maxReward, maxTargetScore, observedAt);
6383
+ const formattedChange = formatRepositoryChange(change, input.output_mode);
6384
+ const { programs: _eventPrograms, targets: _eventTargets, ...repositoryChange } = formattedChange;
6385
+ rankedMatches.push({
6386
+ balancedScore,
6387
+ maxReward,
6388
+ maxTargetScore,
6389
+ observedAt,
6390
+ payload: stripUndefined({
6391
+ repository_change: repositoryChange,
6392
+ programs,
6393
+ targets: targets.slice(0, input.max_targets_per_result).map((target) => input.output_mode === "full" ? target : compactTarget(target)),
6394
+ target_count: targets.length,
6395
+ max_target_score: maxTargetScore,
6396
+ max_program_reward: maxReward,
6397
+ match_score: balancedScore,
6398
+ why_ranked_here: repositoryHuntReasons(maxReward, maxTargetScore, observedAt)
6399
+ })
6400
+ });
6401
+ }
6402
+ rankedMatches.sort((left, right) => compareRepositoryHuntMatches(left, right, input.ranking_mode));
6403
+ const matches = rankedMatches.slice(0, input.max_results).map((match, index) => ({
6404
+ rank: index + 1,
6405
+ ...match.payload
6406
+ }));
6407
+ const matchedPrograms = uniqueProgramsFromRepositoryMatches(matches);
6408
+ const programWarnings = readArray(programPayload.warnings).filter((warning) => typeof warning === "string");
6409
+ if (candidatePrograms.length === 0) {
6410
+ warnings.push("No programs matched the reward and program-level filters. Relax one program filter and retry.");
6411
+ }
6412
+ else if (matches.length === 0) {
6413
+ warnings.push("Programs matched, but no recent repository event also contained a target that passed the target-quality filters. Relax one target or freshness filter and retry.");
6414
+ }
6415
+ const programBudget = readObject(programPayload.upstream_budget);
6416
+ const usedRequests = (readNumber(programBudget?.used) ?? 0) + repository.attempts;
6417
+ return stripUndefined({
6418
+ request_id: randomUUID(),
6419
+ fetched_at: new Date().toISOString(),
6420
+ source_requests: [
6421
+ ...readArray(programPayload.source_requests),
6422
+ {
6423
+ source: "repository_changes",
6424
+ request_id: repository.api.requestId,
6425
+ upstream_request_id: repository.api.upstreamRequestId,
6426
+ page: 1,
6427
+ page_size: repository.pageSize,
6428
+ ...apiSourceMetadata(repository.api)
6429
+ }
6430
+ ],
6431
+ warnings: uniqueStrings([...programWarnings, ...warnings]),
6432
+ filters: stripUndefined({
6433
+ platforms: input.platforms.length > 0 ? input.platforms : undefined,
6434
+ exclude_platforms: input.exclude_platforms.length > 0 ? input.exclude_platforms : undefined,
6435
+ tags: input.tags.length > 0 ? input.tags : undefined,
6436
+ scope_tags: input.scope_tags.length > 0 ? input.scope_tags : undefined,
6437
+ target_types: input.target_types.length > 0 ? input.target_types : undefined,
6438
+ language_tags: input.language_tags.length > 0 ? input.language_tags : undefined,
6439
+ surface_mode: input.surface_mode,
6440
+ opportunity_levels: input.opportunity_levels.length > 0 ? input.opportunity_levels : undefined,
6441
+ min_bounty_max: input.min_bounty_max,
6442
+ require_bounty: input.require_bounty,
6443
+ max_public_report_count: input.max_public_report_count,
6444
+ min_eligible_targets: input.min_eligible_targets,
6445
+ min_total_targets: input.min_total_targets,
6446
+ min_wildcards: input.min_wildcards,
6447
+ min_added_7d: input.min_added_7d,
6448
+ changed_since: input.changed_since,
6449
+ free_submission_only: input.free_submission_only,
6450
+ min_target_score: input.min_target_score,
6451
+ target_opportunity_labels: input.target_opportunity_labels.length > 0 ? input.target_opportunity_labels : undefined,
6452
+ competition_risks: input.competition_risks.length > 0 ? input.competition_risks : undefined,
6453
+ ranking_mode: input.ranking_mode,
6454
+ output_mode: input.output_mode
6455
+ }),
6456
+ upstream_budget: {
6457
+ initial: input.upstream_request_budget,
6458
+ used: usedRequests,
6459
+ remaining: Math.max(0, input.upstream_request_budget - usedRequests)
6460
+ },
6461
+ ranking_scope: readObject(programPayload.ranking_scope),
6462
+ programs: matchedPrograms,
6463
+ matches,
6464
+ meta: {
6465
+ returned: matches.length,
6466
+ matched_programs: matchedPrograms.length,
6467
+ program_candidates: candidatePrograms.length,
6468
+ repository_events_scanned: readArray(readObject(repository.api.data)?.results).length,
6469
+ qualifying_targets_before_result_limit: qualifyingTargetCount,
6470
+ repository_page_size_used: repository.pageSize,
6471
+ repository_request_attempts: repository.attempts,
6472
+ parallel_source_fetch: true
6473
+ }
6474
+ });
6475
+ }
6476
+ async function fetchRepositoryHuntPage(client, input, warnings, maxAttempts) {
6477
+ let pageSize = input.repository_page_size;
6478
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
6479
+ try {
6480
+ const api = await client.getRepositoryChanges(toQuery({
6481
+ include_targets: true,
6482
+ platforms: input.platforms.length > 0 ? input.platforms : undefined,
6483
+ tags: input.tags.length > 0 ? input.tags : undefined,
6484
+ free_submission_only: input.free_submission_only,
6485
+ include_removed: false,
6486
+ include_ineligible: false,
6487
+ include_out_of_scope: false,
6488
+ page: 1,
6489
+ page_size: pageSize
6490
+ }));
6491
+ return { api, pageSize, attempts: attempt };
6492
+ }
6493
+ catch (error) {
6494
+ if (!(error instanceof BBRadarApiError) || error.status !== 413 || attempt >= maxAttempts || pageSize <= 10) {
6495
+ throw error;
6496
+ }
6497
+ const reducedPageSize = Math.max(10, Math.floor(pageSize / 2));
6498
+ warnings.push(`Repository page_size was reduced from ${pageSize} to ${reducedPageSize} after a 413 association-limit response.`);
6499
+ pageSize = reducedPageSize;
6500
+ }
6501
+ }
6502
+ throw new Error("No repository-change response was collected.");
6503
+ }
6504
+ function repositoryHuntTargetMatches(target, input) {
6505
+ if (readBoolean(target.in_scope) !== true || readBoolean(target.eligible_for_bounty) !== true) {
6506
+ return false;
6507
+ }
6508
+ const score = readNumber(target.target_opportunity_score);
6509
+ if (score === undefined || score < input.min_target_score) {
6510
+ return false;
6511
+ }
6512
+ if (input.target_opportunity_labels.length > 0 &&
6513
+ !input.target_opportunity_labels.includes(stringField(target, "target_opportunity_label"))) {
6514
+ return false;
6515
+ }
6516
+ if (input.competition_risks.length > 0 &&
6517
+ !input.competition_risks.includes(stringField(target, "competition_risk"))) {
6518
+ return false;
6519
+ }
6520
+ if (input.target_types.length > 0 && !input.target_types.some((targetType) => targetMatchesRequestedType(target, targetType))) {
6521
+ return false;
6522
+ }
6523
+ if (input.scope_tags.length > 0 && !targetMatchesAnyTags(target, input.scope_tags, "scope_tags")) {
6524
+ return false;
6525
+ }
6526
+ if (input.language_tags.length > 0 && !targetMatchesAnyTags(target, input.language_tags, "language_tags")) {
6527
+ return false;
6528
+ }
6529
+ return true;
6530
+ }
6531
+ function targetMatchesAnyTags(target, requested, field) {
6532
+ const actual = new Set(readStringArrayField(target, field).map(normalizeTag));
6533
+ return requested.map(normalizeTag).some((value) => actual.has(value));
6534
+ }
6535
+ function programMaximumReward(program) {
6536
+ return readNumber(readObject(program.reward_range)?.max) ?? 0;
6537
+ }
6538
+ function repositoryHuntBalancedScore(maxReward, maxTargetScore, observedAt) {
6539
+ const rewardScore = Math.min(100, Math.log10(Math.max(1, maxReward)) / 7 * 100);
6540
+ const ageDays = observedAt > 0 ? Math.max(0, Date.now() - observedAt) / (24 * 60 * 60 * 1000) : 30;
6541
+ const freshnessScore = Math.max(0, 100 - Math.min(30, ageDays) / 30 * 100);
6542
+ return Math.round((maxTargetScore * 0.45 + rewardScore * 0.35 + freshnessScore * 0.2) * 100) / 100;
6543
+ }
6544
+ function repositoryHuntReasons(maxReward, maxTargetScore, observedAt) {
6545
+ const reasons = [`target score up to ${maxTargetScore}`, `maximum reward ${maxReward}`];
6546
+ if (observedAt > 0) {
6547
+ reasons.push(`repository activity observed ${new Date(observedAt).toISOString()}`);
6548
+ }
6549
+ return reasons;
6550
+ }
6551
+ function compareRepositoryHuntMatches(left, right, mode) {
6552
+ if (mode === "highest_reward") {
6553
+ return right.maxReward - left.maxReward || right.maxTargetScore - left.maxTargetScore || right.observedAt - left.observedAt;
6554
+ }
6555
+ if (mode === "best_target") {
6556
+ return right.maxTargetScore - left.maxTargetScore || right.maxReward - left.maxReward || right.observedAt - left.observedAt;
6557
+ }
6558
+ if (mode === "newest") {
6559
+ return right.observedAt - left.observedAt || right.maxTargetScore - left.maxTargetScore || right.maxReward - left.maxReward;
6560
+ }
6561
+ return right.balancedScore - left.balancedScore || right.observedAt - left.observedAt;
6562
+ }
6563
+ function uniqueProgramsFromRepositoryMatches(matches) {
6564
+ const programs = new Map();
6565
+ for (const match of matches) {
6566
+ for (const value of readArray(match.programs)) {
6567
+ const program = readObject(value);
6568
+ const id = stringField(program, "id");
6569
+ if (program && id && !programs.has(id)) {
6570
+ programs.set(id, program);
6571
+ }
6572
+ }
6573
+ }
6574
+ return [...programs.values()];
6575
+ }
6261
6576
  async function findPrograms(client, config, input) {
6262
6577
  const normalized = normalizeFindProgramsInput(input);
6263
6578
  const warnings = normalized.warnings;