@apifuse/provider-sdk 2.1.0-beta.17 → 2.1.0-beta.19

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.
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
+ import { type ChildProcess, spawn } from "node:child_process";
3
4
  import { existsSync, readdirSync, readFileSync } from "node:fs";
4
5
  import { writeFile } from "node:fs/promises";
6
+ import { createServer } from "node:net";
5
7
  import { basename, dirname, join, relative, resolve } from "node:path";
6
8
  import { pathToFileURL } from "node:url";
7
9
 
@@ -15,6 +17,7 @@ import {
15
17
  validateProviderLocaleCatalogs,
16
18
  } from "../src/i18n";
17
19
  import { APIFUSE_DESCRIPTION_KEY_META_KEY } from "../src/schema";
20
+ import { safeParseSchemaSync } from "../src/schema";
18
21
  import { type CheckResult, runChecks } from "./apifuse-check";
19
22
 
20
23
  const TIERS = ["bronze", "silver", "gold", "diamond"] as const;
@@ -35,6 +38,7 @@ export type SubmitCheck = {
35
38
  message: string;
36
39
  remediation?: string;
37
40
  evidence?: string[];
41
+ details?: unknown;
38
42
  };
39
43
 
40
44
  export type SubmitCheckReport = {
@@ -69,6 +73,7 @@ type CliArgs = {
69
73
  isJson: boolean;
70
74
  markdownPath?: string;
71
75
  providerPath?: string;
76
+ smoke: boolean;
72
77
  smokeNote?: string;
73
78
  tier?: BountyTier;
74
79
  };
@@ -76,6 +81,24 @@ type CliArgs = {
76
81
  type SecretFinding = {
77
82
  label: string;
78
83
  file: string;
84
+ line?: number;
85
+ level?: CheckLevel;
86
+ remediation?: string;
87
+ evidence?: string;
88
+ };
89
+
90
+ export type SmokeOperationOutcome = {
91
+ operationId: string;
92
+ status: "success" | "structured_error" | "incoherent";
93
+ httpStatus?: number;
94
+ message: string;
95
+ };
96
+
97
+ export type SmokeResult = {
98
+ measured: true;
99
+ healthOk: boolean;
100
+ bootError?: string;
101
+ operations: SmokeOperationOutcome[];
79
102
  };
80
103
 
81
104
  type SourceFinding = {
@@ -98,14 +121,13 @@ const CATEGORY_MAX_POINTS = {
98
121
  docs: 10,
99
122
  } as const;
100
123
 
101
- const REQUIRED_PUBLIC_PROVIDER_LOCALES = [
102
- "en",
103
- "ko",
104
- ] as const satisfies readonly ProviderLocale[];
124
+ const REQUIRED_PUBLIC_PROVIDER_LOCALES = ["en", "ko"] as const satisfies readonly ProviderLocale[];
105
125
 
106
- const HELP_TEXT = `Usage: apifuse submit-check [path] [--tier bronze|silver|gold|diamond] [--json] [--markdown <path>] [--smoke-note <text>]
126
+ const HELP_TEXT = `Usage: apifuse submit-check [path] [--tier bronze|silver|gold|diamond] [--json] [--markdown <path>] [--smoke]
107
127
  Alias: apifuse bounty-check [path]
108
- Default: apifuse submit-check .`;
128
+ Default: apifuse submit-check .
129
+
130
+ Smoke: --smoke boots the provider dev server, checks /health, and POSTs every operation fixture. APIFUSE__PROVIDER__* env vars enable live upstream calls; without them, structured provider errors can still verify runtime routing. --smoke-note is deprecated and ignored for scoring.`;
109
131
 
110
132
  export async function main() {
111
133
  try {
@@ -120,10 +142,7 @@ export async function main() {
120
142
  const report = await buildSubmitCheckReport(providerRoot, args);
121
143
 
122
144
  if (args.markdownPath) {
123
- await writeFile(
124
- resolve(process.cwd(), args.markdownPath),
125
- renderMarkdown(report),
126
- );
145
+ await writeFile(resolve(process.cwd(), args.markdownPath), renderMarkdown(report));
127
146
  }
128
147
 
129
148
  if (args.isJson) {
@@ -150,7 +169,7 @@ function normalizeArgs(argv: string[]): string[] {
150
169
  }
151
170
 
152
171
  function parseArgs(argv: string[]): CliArgs {
153
- const args: CliArgs = { isJson: false };
172
+ const args: CliArgs = { isJson: false, smoke: false };
154
173
 
155
174
  for (let index = 0; index < argv.length; index += 1) {
156
175
  const arg = argv[index];
@@ -177,6 +196,11 @@ function parseArgs(argv: string[]): CliArgs {
177
196
  continue;
178
197
  }
179
198
 
199
+ if (arg === "--smoke") {
200
+ args.smoke = true;
201
+ continue;
202
+ }
203
+
180
204
  if (arg === "--smoke-note") {
181
205
  args.smokeNote = requireValue(argv, index, arg);
182
206
  index += 1;
@@ -226,9 +250,7 @@ function parseTier(value: string): BountyTier {
226
250
  if (isBountyTier(value)) {
227
251
  return value;
228
252
  }
229
- throw new Error(
230
- `Invalid --tier "${value}". Expected one of: ${TIERS.join(", ")}`,
231
- );
253
+ throw new Error(`Invalid --tier "${value}". Expected one of: ${TIERS.join(", ")}`);
232
254
  }
233
255
 
234
256
  function isBountyTier(value: string): value is BountyTier {
@@ -237,7 +259,7 @@ function isBountyTier(value: string): value is BountyTier {
237
259
 
238
260
  export async function buildSubmitCheckReport(
239
261
  providerRoot: string,
240
- args: { smokeNote?: string; tier?: BountyTier } = {},
262
+ args: { smoke?: boolean; smokeNote?: string; tier?: BountyTier } = {},
241
263
  ): Promise<SubmitCheckReport> {
242
264
  const checks: SubmitCheck[] = [];
243
265
  const baseChecks = await safeRunChecks(providerRoot);
@@ -257,16 +279,17 @@ export async function buildSubmitCheckReport(
257
279
  checks.push(scoreFlatOperationComposition(providerRoot));
258
280
 
259
281
  if (provider) {
282
+ const smokeResult = args.smoke ? await runSubmitCheckSmoke(providerRoot, provider) : undefined;
260
283
  checks.push(scoreCredentialUsage(providerRoot, provider));
261
284
  checks.push(scoreLocaleCatalog(providerRoot, provider));
262
285
  checks.push(scoreOperationMetadata(provider));
263
286
  checks.push(scoreFixtureCoverage(provider));
264
287
  checks.push(scoreHealthCoverage(provider));
265
288
  checks.push(scoreAuthSafety(provider));
266
- checks.push(scoreSmokeEvidence(args.smokeNote));
289
+ checks.push(scoreSmoke(smokeResult, args.smokeNote));
267
290
  checks.push(...scoreProviderDocs(providerRoot));
268
291
  checks.push(scoreRepositoryDx(providerRoot));
269
- checks.push(scoreSecrets(providerRoot));
292
+ checks.push(scoreSecrets(providerRoot, provider));
270
293
  } else {
271
294
  checks.push(
272
295
  blocker(
@@ -279,22 +302,14 @@ export async function buildSubmitCheckReport(
279
302
  );
280
303
  }
281
304
 
282
- const total = clamp(
283
- Math.round(checks.reduce((sum, check) => sum + check.points, 0)),
284
- 0,
285
- 100,
286
- );
305
+ const total = clamp(Math.round(checks.reduce((sum, check) => sum + check.points, 0)), 0, 100);
287
306
  const blockers = checks.filter(
288
307
  (check) => check.level === "blocker" && check.status === "fail",
289
308
  ).length;
290
309
  const warnings = checks.filter((check) => check.status === "warn").length;
291
310
  const passed = checks.filter((check) => check.status === "pass").length;
292
311
  const verdict: Verdict =
293
- blockers > 0
294
- ? "blocked"
295
- : total >= 90 && warnings === 0
296
- ? "ready"
297
- : "reviewable_with_warnings";
312
+ blockers > 0 ? "blocked" : total >= 90 && warnings === 0 ? "ready" : "reviewable_with_warnings";
298
313
 
299
314
  return {
300
315
  schemaVersion: 1,
@@ -335,18 +350,10 @@ function scoreProviderIdSlug(
335
350
  );
336
351
  }
337
352
 
338
- return pass(
339
- "id-slug",
340
- SDK_NATIVE_CATEGORY,
341
- "Provider id uses the short slug.",
342
- 0,
343
- );
353
+ return pass("id-slug", SDK_NATIVE_CATEGORY, "Provider id uses the short slug.", 0);
344
354
  }
345
355
 
346
- const findings = findSourceLineMatches(
347
- providerRoot,
348
- /["'`]apifuse-provider-[a-z0-9-]/i,
349
- );
356
+ const findings = findSourceLineMatches(providerRoot, /["'`]apifuse-provider-[a-z0-9-]/i);
350
357
  if (findings.length > 0) {
351
358
  return blocker(
352
359
  "id-slug",
@@ -358,12 +365,7 @@ function scoreProviderIdSlug(
358
365
  );
359
366
  }
360
367
 
361
- return pass(
362
- "id-slug",
363
- SDK_NATIVE_CATEGORY,
364
- "Provider id uses the short slug.",
365
- 0,
366
- );
368
+ return pass("id-slug", SDK_NATIVE_CATEGORY, "Provider id uses the short slug.", 0);
367
369
  }
368
370
 
369
371
  function scoreNoVendorShim(providerRoot: string): SubmitCheck {
@@ -388,10 +390,7 @@ function scoreNoVendorShim(providerRoot: string): SubmitCheck {
388
390
  }
389
391
 
390
392
  function scoreNoVendorImport(providerRoot: string): SubmitCheck {
391
- const findings = findSourceLineMatches(
392
- providerRoot,
393
- /from\s+["'][^"']*vendor\//,
394
- );
393
+ const findings = findSourceLineMatches(providerRoot, /from\s+["'][^"']*vendor\//);
395
394
  if (findings.length > 0) {
396
395
  return blocker(
397
396
  "no-vendor-import",
@@ -424,33 +423,24 @@ function scoreDescribeKey(providerRoot: string): SubmitCheck {
424
423
  );
425
424
  }
426
425
 
427
- return pass(
428
- "describe-key",
429
- SDK_NATIVE_CATEGORY,
430
- "Schema descriptions use describeKey.",
431
- 0,
432
- );
426
+ return pass("describe-key", SDK_NATIVE_CATEGORY, "Schema descriptions use describeKey.", 0);
433
427
  }
434
428
 
435
429
  function scoreNoRawFetch(providerRoot: string): SubmitCheck {
436
430
  const findings = findSourceLineMatches(providerRoot, /(?<![.\w])fetch\s*\(/);
437
431
  if (findings.length > 0) {
432
+ const evidence = formatSourceFindings(findings);
438
433
  return blocker(
439
434
  "no-raw-fetch",
440
435
  SDK_NATIVE_CATEGORY,
441
436
  "Provider source calls raw fetch().",
442
- "Replace raw fetch() with ctx.stealth.fetch() (cloud-IP-blocked otherwise) or ctx.http for non-stealth calls.",
437
+ `Replace raw fetch() in ${evidence.join(", ")} with ctx.stealth.fetch() for stealth/cloud-IP-sensitive calls or ctx.http.get/post/request for ordinary HTTP calls.`,
443
438
  0,
444
- formatSourceFindings(findings),
439
+ evidence,
445
440
  );
446
441
  }
447
442
 
448
- return pass(
449
- "no-raw-fetch",
450
- SDK_NATIVE_CATEGORY,
451
- "Provider source avoids raw fetch().",
452
- 0,
453
- );
443
+ return pass("no-raw-fetch", SDK_NATIVE_CATEGORY, "Provider source avoids raw fetch().", 0);
454
444
  }
455
445
 
456
446
  const REDUNDANT_RUNTIME_GUARD_PATTERNS: readonly RegExp[] = [
@@ -460,10 +450,7 @@ const REDUNDANT_RUNTIME_GUARD_PATTERNS: readonly RegExp[] = [
460
450
  const SDK_CONTEXT_METHOD_ALIAS_PATTERN =
461
451
  /\bconst\s+(\w+)\s*=\s*ctx\.(?:stealth|http|cache|state|browser|trace|auth|stt|choice)\.(?:\w+)/;
462
452
 
463
- function hasRedundantRuntimeGuard(
464
- line: string,
465
- remainingLines: readonly string[],
466
- ): boolean {
453
+ function hasRedundantRuntimeGuard(line: string, remainingLines: readonly string[]): boolean {
467
454
  if (REDUNDANT_RUNTIME_GUARD_PATTERNS.some((pattern) => pattern.test(line))) {
468
455
  return true;
469
456
  }
@@ -474,12 +461,8 @@ function hasRedundantRuntimeGuard(
474
461
  return false;
475
462
  }
476
463
 
477
- const guardPattern = new RegExp(
478
- `(?:typeof\\s+${alias}\\s*!==\\s*["']function["']|!${alias}\\b)`,
479
- );
480
- return remainingLines
481
- .slice(0, 8)
482
- .some((candidate) => guardPattern.test(candidate));
464
+ const guardPattern = new RegExp(`(?:typeof\\s+${alias}\\s*!==\\s*["']function["']|!${alias}\\b)`);
465
+ return remainingLines.slice(0, 8).some((candidate) => guardPattern.test(candidate));
483
466
  }
484
467
 
485
468
  function scoreNoRedundantRuntimeGuards(providerRoot: string): SubmitCheck {
@@ -580,11 +563,7 @@ function scoreAsAssertionCount(providerRoot: string): SubmitCheck {
580
563
 
581
564
  // Returns true when `findingLine` (1-based) or the line directly above it
582
565
  // carries an `// @apifuse-allow <ruleId>:` acknowledgement comment.
583
- function hasAllowOverride(
584
- lines: readonly string[],
585
- findingLine: number,
586
- ruleId: string,
587
- ): boolean {
566
+ function hasAllowOverride(lines: readonly string[], findingLine: number, ruleId: string): boolean {
588
567
  const pattern = new RegExp(`@apifuse-allow\\s+${ruleId}\\b`);
589
568
  const current = lines[findingLine - 1];
590
569
  const previous = lines[findingLine - 2];
@@ -635,11 +614,7 @@ function escapeHatchResult(
635
614
  return pass(ruleId, SDK_NATIVE_CATEGORY, copy.passMessage, 0);
636
615
  }
637
616
 
638
- const { violations, overridden } = partitionAllowOverrides(
639
- providerRoot,
640
- findings,
641
- ruleId,
642
- );
617
+ const { violations, overridden } = partitionAllowOverrides(providerRoot, findings, ruleId);
643
618
 
644
619
  if (violations.length > 0) {
645
620
  return blocker(
@@ -952,9 +927,7 @@ function inputKeyIsSchemaField(source: string, propIndex: number): boolean {
952
927
  // across unrelated modules from producing false positives).
953
928
  function fileImportsBinding(source: string, name: string): boolean {
954
929
  const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
955
- return new RegExp(`\\bimport\\b[^;]*\\b${escaped}\\b[^;]*\\bfrom\\b`).test(
956
- source,
957
- );
930
+ return new RegExp(`\\bimport\\b[^;]*\\b${escaped}\\b[^;]*\\bfrom\\b`).test(source);
958
931
  }
959
932
 
960
933
  // Resolves the ORIGINAL exported name for a local binding `localName`. When the
@@ -998,11 +971,7 @@ function scoreUnsafeInputPassthrough(providerRoot: string): SubmitCheck {
998
971
  passthroughByFile.set(filePath, localMap);
999
972
  const constDecl =
1000
973
  /(?:^|\n)[ \t]*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=\n]+)?\s*=/g;
1001
- for (
1002
- let match = constDecl.exec(source);
1003
- match !== null;
1004
- match = constDecl.exec(source)
1005
- ) {
974
+ for (let match = constDecl.exec(source); match !== null; match = constDecl.exec(source)) {
1006
975
  const name = match[1];
1007
976
  if (name === undefined) {
1008
977
  continue;
@@ -1041,11 +1010,7 @@ function scoreUnsafeInputPassthrough(providerRoot: string): SubmitCheck {
1041
1010
  const relPath = toRelativeProviderPath(providerRoot, filePath);
1042
1011
 
1043
1012
  const inputProp = /\binput\s*:\s*/g;
1044
- for (
1045
- let match = inputProp.exec(source);
1046
- match !== null;
1047
- match = inputProp.exec(source)
1048
- ) {
1013
+ for (let match = inputProp.exec(source); match !== null; match = inputProp.exec(source)) {
1049
1014
  // Skip `input` keys that are fields inside a zod schema body (e.g. an
1050
1015
  // upstream payload modelled as `z.object({ input: ... })`). Only an
1051
1016
  // operation's public `input:` property is in scope for this rule.
@@ -1073,9 +1038,7 @@ function scoreUnsafeInputPassthrough(providerRoot: string): SubmitCheck {
1073
1038
  // Imported binding: map a possible `orig as refName` alias
1074
1039
  // back to the exported name the provider-wide map is keyed by.
1075
1040
  const originalName = importedOriginalName(source, refName);
1076
- const site =
1077
- passthroughConsts.get(refName) ??
1078
- passthroughConsts.get(originalName);
1041
+ const site = passthroughConsts.get(refName) ?? passthroughConsts.get(originalName);
1079
1042
  if (site) {
1080
1043
  push(site);
1081
1044
  }
@@ -1121,8 +1084,7 @@ function scoreUnjustifiedLooseSchema(providerRoot: string): SubmitCheck {
1121
1084
  // A `//` justification comment on the same line or the line above
1122
1085
  // (including the `@apifuse-allow loose-schema:` form) acknowledges it.
1123
1086
  const previous = lines[index - 1];
1124
- const justified =
1125
- line.includes("//") || previous?.trim().startsWith("//") === true;
1087
+ const justified = line.includes("//") || previous?.trim().startsWith("//") === true;
1126
1088
  if (!justified) {
1127
1089
  findings.push({
1128
1090
  file: toRelativeProviderPath(providerRoot, filePath),
@@ -1133,8 +1095,7 @@ function scoreUnjustifiedLooseSchema(providerRoot: string): SubmitCheck {
1133
1095
  }
1134
1096
 
1135
1097
  return escapeHatchResult(providerRoot, "unjustified-loose-schema", findings, {
1136
- blockerMessage:
1137
- "Loose schema (z.record/z.unknown/z.any) used without justification.",
1098
+ blockerMessage: "Loose schema (z.record/z.unknown/z.any) used without justification.",
1138
1099
  remediation:
1139
1100
  "Model the real shape with a typed zod schema. If the upstream payload is genuinely arbitrary, add a `// <reason>` comment or `// @apifuse-allow loose-schema: <reason>` on the line above.",
1140
1101
  passMessage: "Loose schemas are justified or absent.",
@@ -1163,24 +1124,18 @@ function spreadIdentifierResolvesToFactory(
1163
1124
  let sawDeclaration = false;
1164
1125
  for (const filePath of [
1165
1126
  indexPath,
1166
- ...listNonTestTypeScriptFiles(providerRoot).filter(
1167
- (p) => resolve(p) !== resolve(indexPath),
1168
- ),
1127
+ ...listNonTestTypeScriptFiles(providerRoot).filter((p) => resolve(p) !== resolve(indexPath)),
1169
1128
  ]) {
1170
1129
  if (!existsSync(filePath)) {
1171
1130
  continue;
1172
1131
  }
1173
- const fileSource =
1174
- filePath === indexPath ? indexSource : readFileSync(filePath, "utf8");
1132
+ const fileSource = filePath === indexPath ? indexSource : readFileSync(filePath, "utf8");
1175
1133
  const re = new RegExp(declRe.source, "g");
1176
1134
  for (let m = re.exec(fileSource); m !== null; m = re.exec(fileSource)) {
1177
1135
  sawDeclaration = true;
1178
- const expr = unwrapParens(
1179
- balancedValueExpression(fileSource, m.index + m[0].length).trim(),
1180
- );
1136
+ const expr = unwrapParens(balancedValueExpression(fileSource, m.index + m[0].length).trim());
1181
1137
  const isFactory =
1182
- (/^[A-Za-z_$][\w$.]*\s*\(/.test(expr) ||
1183
- hasTopLevelFactorySpread(expr)) &&
1138
+ (/^[A-Za-z_$][\w$.]*\s*\(/.test(expr) || hasTopLevelFactorySpread(expr)) &&
1184
1139
  !isTransparentObjectReshape(expr);
1185
1140
  if (isFactory) {
1186
1141
  return true;
@@ -1232,9 +1187,7 @@ function scoreFlatOperationComposition(providerRoot: string): SubmitCheck {
1232
1187
  if (inlineDefault) {
1233
1188
  defineParenIndex = inlineDefault.index + inlineDefault[0].length - 1; // points at `(`
1234
1189
  } else {
1235
- const namedDefault = /\bexport\s+default\s+([A-Za-z_$][\w$]*)\s*;?/.exec(
1236
- source,
1237
- );
1190
+ const namedDefault = /\bexport\s+default\s+([A-Za-z_$][\w$]*)\s*;?/.exec(source);
1238
1191
  const exportedName = namedDefault?.[1];
1239
1192
  if (exportedName !== undefined) {
1240
1193
  const namedDecl = new RegExp(
@@ -1308,9 +1261,7 @@ function scoreFlatOperationComposition(providerRoot: string): SubmitCheck {
1308
1261
  // Search index.ts first (its line attribution wins), then siblings.
1309
1262
  const searchOrder = [
1310
1263
  indexPath,
1311
- ...listNonTestTypeScriptFiles(providerRoot).filter(
1312
- (p) => resolve(p) !== resolve(indexPath),
1313
- ),
1264
+ ...listNonTestTypeScriptFiles(providerRoot).filter((p) => resolve(p) !== resolve(indexPath)),
1314
1265
  ];
1315
1266
 
1316
1267
  // Collect EVERY same-named declaration across the submission and classify
@@ -1330,23 +1281,15 @@ function scoreFlatOperationComposition(providerRoot: string): SubmitCheck {
1330
1281
  if (!existsSync(filePath)) {
1331
1282
  continue;
1332
1283
  }
1333
- const fileSource =
1334
- filePath === indexPath ? source : readFileSync(filePath, "utf8");
1284
+ const fileSource = filePath === indexPath ? source : readFileSync(filePath, "utf8");
1335
1285
  const relPath = toRelativeProviderPath(providerRoot, filePath);
1336
1286
 
1337
1287
  const declRe = new RegExp(aliasDecl.source, "g");
1338
- for (
1339
- let m = declRe.exec(fileSource);
1340
- m !== null;
1341
- m = declRe.exec(fileSource)
1342
- ) {
1288
+ for (let m = declRe.exec(fileSource); m !== null; m = declRe.exec(fileSource)) {
1343
1289
  const valueStart = m.index + m[0].length;
1344
- const expr = unwrapParens(
1345
- balancedValueExpression(fileSource, valueStart).trim(),
1346
- );
1290
+ const expr = unwrapParens(balancedValueExpression(fileSource, valueStart).trim());
1347
1291
  const isFactory =
1348
- (/^[A-Za-z_$][\w$.]*\s*\(/.test(expr) ||
1349
- hasTopLevelFactorySpread(expr)) &&
1292
+ (/^[A-Za-z_$][\w$.]*\s*\(/.test(expr) || hasTopLevelFactorySpread(expr)) &&
1350
1293
  !isTransparentObjectReshape(expr);
1351
1294
  candidates.push({
1352
1295
  expr,
@@ -1356,11 +1299,7 @@ function scoreFlatOperationComposition(providerRoot: string): SubmitCheck {
1356
1299
  });
1357
1300
  }
1358
1301
  const destructRe = new RegExp(destructured.source, "g");
1359
- for (
1360
- let m = destructRe.exec(fileSource);
1361
- m !== null;
1362
- m = destructRe.exec(fileSource)
1363
- ) {
1302
+ for (let m = destructRe.exec(fileSource); m !== null; m = destructRe.exec(fileSource)) {
1364
1303
  candidates.push({
1365
1304
  expr: `${m[1]}(`,
1366
1305
  line: offsetToLine(fileSource, m.index),
@@ -1388,9 +1327,9 @@ function scoreFlatOperationComposition(providerRoot: string): SubmitCheck {
1388
1327
  // the unresolved import as a factory-composed (non-static) shape rather
1389
1328
  // than silently passing.
1390
1329
  if (!resolved) {
1391
- const importMatch = new RegExp(
1392
- `\\bimport\\b[^;]*\\b${aliasName}\\b[^;]*\\bfrom\\b`,
1393
- ).exec(source);
1330
+ const importMatch = new RegExp(`\\bimport\\b[^;]*\\b${aliasName}\\b[^;]*\\bfrom\\b`).exec(
1331
+ source,
1332
+ );
1394
1333
  if (importMatch) {
1395
1334
  effective = `${aliasName}(`;
1396
1335
  effectiveLine = offsetToLine(source, importMatch.index);
@@ -1405,8 +1344,7 @@ function scoreFlatOperationComposition(providerRoot: string): SubmitCheck {
1405
1344
  // inspect depth-1 entries so that ordinary spreads deep inside operation
1406
1345
  // handler bodies (e.g. `{ ...headers }`, `...arr.map(...)`) are NOT mistaken
1407
1346
  // for a top-level factory composition of the operations map itself.
1408
- const hasFactorySpread =
1409
- effective !== undefined && hasTopLevelFactorySpread(effective);
1347
+ const hasFactorySpread = effective !== undefined && hasTopLevelFactorySpread(effective);
1410
1348
  // A spread of a bare identifier (`{ ...hidden }`) is static ONLY when that
1411
1349
  // identifier resolves to a non-factory declaration. Resolve each top-level
1412
1350
  // spread identifier so an opaque factory map laundered through a variable
@@ -1417,18 +1355,14 @@ function scoreFlatOperationComposition(providerRoot: string): SubmitCheck {
1417
1355
  spreadIdentifierResolvesToFactory(providerRoot, indexPath, source, name),
1418
1356
  );
1419
1357
  const isStaticLiteral =
1420
- effective?.startsWith("{") === true &&
1421
- !hasFactorySpread &&
1422
- !hasFactorySpreadIdentifier;
1358
+ effective?.startsWith("{") === true && !hasFactorySpread && !hasFactorySpreadIdentifier;
1423
1359
  // A call expression `ident(...)` (factory) or a factory-spread literal is
1424
1360
  // the rejected, non-static shape — UNLESS it is the stdlib
1425
1361
  // `Object.fromEntries(Object.entries(<source-visible obj>)...)` reshape,
1426
1362
  // whose op set is still enumerable from source (verified golden pattern).
1427
1363
  const isFactoryCall =
1428
1364
  effective !== undefined &&
1429
- (/^[A-Za-z_$][\w$.]*\s*\(/.test(effective) ||
1430
- hasFactorySpread ||
1431
- hasFactorySpreadIdentifier) &&
1365
+ (/^[A-Za-z_$][\w$.]*\s*\(/.test(effective) || hasFactorySpread || hasFactorySpreadIdentifier) &&
1432
1366
  !isTransparentObjectReshape(effective);
1433
1367
 
1434
1368
  if (isFactoryCall && !isStaticLiteral) {
@@ -1436,19 +1370,13 @@ function scoreFlatOperationComposition(providerRoot: string): SubmitCheck {
1436
1370
  // `// @apifuse-allow flat-operation-composition: <reason>` comment on
1437
1371
  // the reported line (or the line above) downgrades this blocker to a
1438
1372
  // counted warning, consistent with the other structural rules.
1439
- return escapeHatchResult(
1440
- providerRoot,
1441
- ruleId,
1442
- [{ file: effectiveFile, line: effectiveLine }],
1443
- {
1444
- blockerMessage:
1445
- "defineProvider operations are composed by a factory call instead of a static object literal.",
1446
- remediation:
1447
- "Declare operations as a static literal: defineProvider({ operations: { 'op-id': defineOperation({...}) } }). The provider-registry AST gate requires static runtime/operations; factory composition fails the registry build. If composition is unavoidable, add `// @apifuse-allow flat-operation-composition: <reason>`.",
1448
- passMessage:
1449
- "defineProvider declares operations as a static object literal.",
1450
- },
1451
- );
1373
+ return escapeHatchResult(providerRoot, ruleId, [{ file: effectiveFile, line: effectiveLine }], {
1374
+ blockerMessage:
1375
+ "defineProvider operations are composed by a factory call instead of a static object literal.",
1376
+ remediation:
1377
+ "Declare operations as a static literal: defineProvider({ operations: { 'op-id': defineOperation({...}) } }). The provider-registry AST gate requires static runtime/operations; factory composition fails the registry build. If composition is unavoidable, add `// @apifuse-allow flat-operation-composition: <reason>`.",
1378
+ passMessage: "defineProvider declares operations as a static object literal.",
1379
+ });
1452
1380
  }
1453
1381
 
1454
1382
  return pass(
@@ -1459,18 +1387,11 @@ function scoreFlatOperationComposition(providerRoot: string): SubmitCheck {
1459
1387
  );
1460
1388
  }
1461
1389
 
1462
- function scoreCredentialUsage(
1463
- providerRoot: string,
1464
- provider: ProviderDefinition,
1465
- ): SubmitCheck {
1466
- const credentialReferences = findSourceLineMatches(
1467
- providerRoot,
1468
- /ctx\.credential/,
1469
- );
1390
+ function scoreCredentialUsage(providerRoot: string, provider: ProviderDefinition): SubmitCheck {
1391
+ const credentialReferences = findSourceLineMatches(providerRoot, /ctx\.credential/);
1470
1392
  const authMode = provider.auth?.mode ?? "none";
1471
1393
  const credentialKeys = provider.credential?.keys ?? [];
1472
- const storesProviderCredential =
1473
- authMode !== "none" || credentialKeys.length > 0;
1394
+ const storesProviderCredential = authMode !== "none" || credentialKeys.length > 0;
1474
1395
 
1475
1396
  if (storesProviderCredential && credentialReferences.length === 0) {
1476
1397
  return {
@@ -1480,8 +1401,7 @@ function scoreCredentialUsage(
1480
1401
  status: "warn",
1481
1402
  points: 0,
1482
1403
  maxPoints: 0,
1483
- message:
1484
- "Credential-backed provider does not reference credential persistence in source.",
1404
+ message: "Credential-backed provider does not reference credential persistence in source.",
1485
1405
  remediation:
1486
1406
  "Persist provider session state through the SDK credential context instead of process-local state. See providers/catchtable for the reference pattern.",
1487
1407
  };
@@ -1494,9 +1414,7 @@ function scoreCredentialUsage(
1494
1414
  ? "Provider does not declare reusable credentials."
1495
1415
  : "Credential-backed provider references ctx.credential.",
1496
1416
  0,
1497
- credentialReferences.length > 0
1498
- ? formatSourceFindings(credentialReferences)
1499
- : undefined,
1417
+ credentialReferences.length > 0 ? formatSourceFindings(credentialReferences) : undefined,
1500
1418
  );
1501
1419
  }
1502
1420
 
@@ -1504,9 +1422,7 @@ function findSourceLineMatches(
1504
1422
  providerRoot: string,
1505
1423
  pattern: RegExp | ((line: string) => boolean),
1506
1424
  ): SourceFinding[] {
1507
- return findSourceFindings(providerRoot, (line) =>
1508
- matchesLinePattern(line, pattern),
1509
- );
1425
+ return findSourceFindings(providerRoot, (line) => matchesLinePattern(line, pattern));
1510
1426
  }
1511
1427
 
1512
1428
  function findSourceFindings(
@@ -1533,10 +1449,7 @@ function findSourceFindings(
1533
1449
  return findings;
1534
1450
  }
1535
1451
 
1536
- function matchesLinePattern(
1537
- line: string,
1538
- pattern: RegExp | ((line: string) => boolean),
1539
- ): boolean {
1452
+ function matchesLinePattern(line: string, pattern: RegExp | ((line: string) => boolean)): boolean {
1540
1453
  return typeof pattern === "function" ? pattern(line) : pattern.test(line);
1541
1454
  }
1542
1455
 
@@ -1590,11 +1503,7 @@ function collectNonTestTypeScriptFiles(
1590
1503
  }
1591
1504
  continue;
1592
1505
  }
1593
- if (
1594
- entry.isFile() &&
1595
- relativePath.endsWith(".ts") &&
1596
- !isExcludedTestSource(relativePath)
1597
- ) {
1506
+ if (entry.isFile() && relativePath.endsWith(".ts") && !isExcludedTestSource(relativePath)) {
1598
1507
  files.push(entryPath);
1599
1508
  }
1600
1509
  }
@@ -1609,9 +1518,7 @@ function isScannableProviderSourceFile(relativePath: string): boolean {
1609
1518
  }
1610
1519
 
1611
1520
  function shouldScanSourceDirectory(relativePath: string): boolean {
1612
- return ![".git", "node_modules", "dist", "build", "coverage"].includes(
1613
- relativePath,
1614
- );
1521
+ return ![".git", "node_modules", "dist", "build", "coverage"].includes(relativePath);
1615
1522
  }
1616
1523
 
1617
1524
  function isExcludedTestSource(relativePath: string): boolean {
@@ -1624,10 +1531,7 @@ function isExcludedTestSource(relativePath: string): boolean {
1624
1531
  );
1625
1532
  }
1626
1533
 
1627
- function toRelativeProviderPath(
1628
- providerRoot: string,
1629
- filePath: string,
1630
- ): string {
1534
+ function toRelativeProviderPath(providerRoot: string, filePath: string): string {
1631
1535
  return relative(providerRoot, filePath).replaceAll("\\", "/");
1632
1536
  }
1633
1537
 
@@ -1673,9 +1577,7 @@ function scoreRepositoryDx(providerRoot: string): SubmitCheck {
1673
1577
  };
1674
1578
  }
1675
1579
 
1676
- function readPackageScripts(
1677
- packageJsonPath: string,
1678
- ): Record<string, unknown> | undefined {
1580
+ function readPackageScripts(packageJsonPath: string): Record<string, unknown> | undefined {
1679
1581
  if (!existsSync(packageJsonPath)) {
1680
1582
  return undefined;
1681
1583
  }
@@ -1790,6 +1692,10 @@ function scoreManagedBrowserRuntime(providerRoot: string): SubmitCheck {
1790
1692
  function scoreBaseChecks(results: CheckResult[]): SubmitCheck[] {
1791
1693
  const failed = results.filter((result) => !result.passed);
1792
1694
  if (failed.length > 0) {
1695
+ const remediation = [
1696
+ "Run `bunx apifuse check .` from the provider root.",
1697
+ ...Array.from(new Set(failed.map(baseCheckRemediation))),
1698
+ ].join(" ");
1793
1699
  return [
1794
1700
  {
1795
1701
  id: "base-checks",
@@ -1799,8 +1705,7 @@ function scoreBaseChecks(results: CheckResult[]): SubmitCheck[] {
1799
1705
  points: 0,
1800
1706
  maxPoints: CATEGORY_MAX_POINTS.definition,
1801
1707
  message: "Base provider checks failed.",
1802
- remediation:
1803
- "Run `bun run check` and fix every failing item before bounty submission.",
1708
+ remediation,
1804
1709
  evidence: failed.map((result) =>
1805
1710
  redact(`${result.message}: ${(result.details ?? []).join("; ")}`),
1806
1711
  ),
@@ -1822,10 +1727,32 @@ function scoreBaseChecks(results: CheckResult[]): SubmitCheck[] {
1822
1727
  ];
1823
1728
  }
1824
1729
 
1825
- function scoreLocaleCatalog(
1826
- providerRoot: string,
1827
- provider: ProviderDefinition,
1828
- ): SubmitCheck {
1730
+ function baseCheckRemediation(result: CheckResult): string {
1731
+ switch (result.message) {
1732
+ case "index.ts exists and exports default defineProvider":
1733
+ return "Fix `index.ts` so it default-exports `defineProvider({...})`.";
1734
+ case "All operations have handler, input, output":
1735
+ return "For each operation named in evidence, add `handler`, `input`, and `output` fields to `defineProvider({ operations })`.";
1736
+ case "All operations have fixtures":
1737
+ return "For each operation named in evidence, add `fixtures.request` and `fixtures.response` values that exercise the operation schemas.";
1738
+ case "Zod schemas parse fixtures without error":
1739
+ return "Update the failing fixture values or their zod schemas until `fixtures.request` and `fixtures.response` parse cleanly.";
1740
+ case "Provider authoring lint has no error-level diagnostics":
1741
+ return "Fix each lint diagnostic shown in evidence, then rerun `bunx apifuse check .`.";
1742
+ case "Provider metadata is declared in defineProvider":
1743
+ return "Fill the missing `defineProvider` metadata fields: `id`, `meta.displayName`, `meta.category`, `runtime`, and `auth.mode`.";
1744
+ case "Dockerfile exists":
1745
+ return "Add a provider-root `Dockerfile` based on the current `apifuse create` template.";
1746
+ case "package.json exists with @apifuse/provider-sdk dependency":
1747
+ return "Add `@apifuse/provider-sdk` to `package.json` dependencies.";
1748
+ case "Base provider checks can run":
1749
+ return "Fix the import/runtime error shown in evidence so `apifuse check` can load the provider.";
1750
+ default:
1751
+ return `Fix the failing base check "${result.message}" shown in evidence.`;
1752
+ }
1753
+ }
1754
+
1755
+ function scoreLocaleCatalog(providerRoot: string, provider: ProviderDefinition): SubmitCheck {
1829
1756
  const requiredKeys = collectProviderRequiredLocaleKeys(provider);
1830
1757
  if (requiredKeys.length === 0) {
1831
1758
  return pass(
@@ -1856,9 +1783,7 @@ function scoreLocaleCatalog(
1856
1783
  "Provider locale catalog is missing required public-provider copy.",
1857
1784
  "Add provider-local locales/en.json and locales/ko.json values for every provider metadata key, operation descriptionKey, and .describeKey() or describeKey() schema field.",
1858
1785
  0,
1859
- validation.issues.map(
1860
- (issue) => `${issue.locale}:${issue.key}: ${issue.message}`,
1861
- ),
1786
+ validation.issues.map((issue) => `${issue.locale}:${issue.key}: ${issue.message}`),
1862
1787
  );
1863
1788
  }
1864
1789
  } catch (error) {
@@ -1881,9 +1806,7 @@ function scoreLocaleCatalog(
1881
1806
  );
1882
1807
  }
1883
1808
 
1884
- function collectProviderRequiredLocaleKeys(
1885
- provider: ProviderDefinition,
1886
- ): string[] {
1809
+ function collectProviderRequiredLocaleKeys(provider: ProviderDefinition): string[] {
1887
1810
  const keys = new Set<string>();
1888
1811
 
1889
1812
  addLocaleKeys(keys, [
@@ -1946,10 +1869,7 @@ function collectSchemaDescriptionKeys(schema: unknown): string[] {
1946
1869
  return keys;
1947
1870
  }
1948
1871
 
1949
- function collectJsonSchemaDescriptionKeys(
1950
- schema: Record<string, unknown>,
1951
- keys: string[],
1952
- ): void {
1872
+ function collectJsonSchemaDescriptionKeys(schema: Record<string, unknown>, keys: string[]): void {
1953
1873
  const descriptionKey = schema[APIFUSE_DESCRIPTION_KEY_META_KEY];
1954
1874
  if (typeof descriptionKey === "string" && descriptionKey.length > 0) {
1955
1875
  keys.push(descriptionKey);
@@ -1981,8 +1901,7 @@ function scoreOperationMetadata(provider: ProviderDefinition): SubmitCheck {
1981
1901
  // is enforced at registry catalog-build time, matching how lintOperation
1982
1902
  // skips the raw-description min-length rule when a descriptionKey is set.
1983
1903
  const hasDescriptionKey =
1984
- typeof operation.descriptionKey === "string" &&
1985
- operation.descriptionKey.length > 0;
1904
+ typeof operation.descriptionKey === "string" && operation.descriptionKey.length > 0;
1986
1905
  if (hasDescriptionKey) return false;
1987
1906
  return true;
1988
1907
  })
@@ -2000,14 +1919,12 @@ function scoreOperationMetadata(provider: ProviderDefinition): SubmitCheck {
2000
1919
  points: 0,
2001
1920
  maxPoints: CATEGORY_MAX_POINTS.operations,
2002
1921
  message: "One or more operations have weak descriptions.",
2003
- remediation:
2004
- "Add 150+ character English descriptions explaining when to use, when not to use, outputs, and caveats.",
1922
+ remediation: `For ${weakDescriptions.join(", ")}, add an operation \`descriptionKey\` backed by \`locales/en.json\` and \`locales/ko.json\`, or add a 150+ character \`description\` explaining when to use it, when not to use it, outputs, and caveats.`,
2005
1923
  evidence: weakDescriptions,
2006
1924
  };
2007
1925
  }
2008
1926
 
2009
- const points =
2010
- missingAnnotations.length > 0 ? 11 : CATEGORY_MAX_POINTS.operations;
1927
+ const points = missingAnnotations.length > 0 ? 11 : CATEGORY_MAX_POINTS.operations;
2011
1928
  return {
2012
1929
  id: "operation-metadata",
2013
1930
  category: "operations",
@@ -2021,30 +1938,25 @@ function scoreOperationMetadata(provider: ProviderDefinition): SubmitCheck {
2021
1938
  : "Operation descriptions and metadata are review-ready.",
2022
1939
  remediation:
2023
1940
  missingAnnotations.length > 0
2024
- ? "Add annotations such as readOnly, destructive, idempotent, openWorld, rateLimit, or timeoutMs where applicable."
1941
+ ? `For ${missingAnnotations.join(", ")}, add \`annotations\` with the applicable safety fields, such as \`readOnly\`, \`destructive\`, \`idempotent\`, \`openWorld\`, \`rateLimit\`, or \`timeoutMs\`.`
2025
1942
  : undefined,
2026
1943
  evidence:
2027
1944
  missingAnnotations.length > 0
2028
- ? missingAnnotations.map(
2029
- (operationId) => `${operationId}: missing annotations`,
2030
- )
1945
+ ? missingAnnotations.map((operationId) => `${operationId}: missing annotations`)
2031
1946
  : operations.map(([operationId]) => operationId),
2032
1947
  };
2033
1948
  }
2034
1949
 
2035
1950
  function scoreFixtureCoverage(provider: ProviderDefinition): SubmitCheck {
2036
1951
  const missing = Object.entries(provider.operations)
2037
- .filter(
2038
- ([, operation]) =>
2039
- !operation.fixtures?.request || !operation.fixtures?.response,
2040
- )
1952
+ .filter(([, operation]) => !operation.fixtures?.request || !operation.fixtures?.response)
2041
1953
  .map(([operationId]) => operationId);
2042
1954
  if (missing.length > 0) {
2043
1955
  return blocker(
2044
1956
  "fixtures",
2045
1957
  "fixtures",
2046
1958
  "One or more operations are missing bidirectional fixtures.",
2047
- "Add fixtures.request and fixtures.response that parse against operation schemas.",
1959
+ `For ${missing.join(", ")}, add \`fixtures.request\` and \`fixtures.response\` values that parse against the operation input and output schemas.`,
2048
1960
  CATEGORY_MAX_POINTS.fixtures,
2049
1961
  missing,
2050
1962
  );
@@ -2078,9 +1990,7 @@ function scoreHealthCoverage(provider: ProviderDefinition): SubmitCheck {
2078
1990
  generatedStarter.push(operationId);
2079
1991
  }
2080
1992
  if (
2081
- /(todo|later|tbd|test fixture|unit test|placeholder|not sure|skip for test)/i.test(
2082
- reason,
2083
- )
1993
+ /(todo|later|tbd|test fixture|unit test|placeholder|not sure|skip for test)/i.test(reason)
2084
1994
  ) {
2085
1995
  placeholder.push(operationId);
2086
1996
  }
@@ -2092,7 +2002,7 @@ function scoreHealthCoverage(provider: ProviderDefinition): SubmitCheck {
2092
2002
  "health-coverage",
2093
2003
  "health",
2094
2004
  "One or more operations lack healthCheck or healthCheckUnsupported.",
2095
- "Declare a safe healthCheck for read-only upstream probes or a specific healthCheckUnsupported.reason.",
2005
+ `For ${missing.join(", ")}, add \`healthCheck: { interval, cases }\` for safe read-only upstream probes, or add \`healthCheckUnsupported: { reason: "<specific reason>" }\`.`,
2096
2006
  CATEGORY_MAX_POINTS.health,
2097
2007
  missing,
2098
2008
  );
@@ -2107,8 +2017,7 @@ function scoreHealthCoverage(provider: ProviderDefinition): SubmitCheck {
2107
2017
  points: 8,
2108
2018
  maxPoints: CATEGORY_MAX_POINTS.health,
2109
2019
  message: "Some healthCheckUnsupported reasons look placeholder-like.",
2110
- remediation:
2111
- "Replace placeholder rationale with a specific reason such as destructive mutation, paid call, credential sensitivity, or upstream flakiness.",
2020
+ remediation: `For ${placeholder.join(", ")}, replace the placeholder \`healthCheckUnsupported.reason\` with a specific reason such as destructive mutation, paid call, credential sensitivity, or upstream flakiness.`,
2112
2021
  evidence: placeholder,
2113
2022
  };
2114
2023
  }
@@ -2123,8 +2032,7 @@ function scoreHealthCoverage(provider: ProviderDefinition): SubmitCheck {
2123
2032
  maxPoints: CATEGORY_MAX_POINTS.health,
2124
2033
  message:
2125
2034
  "Generated starter operation health rationale is present; replace starter logic before bounty submission.",
2126
- remediation:
2127
- "Replace `ping` with real upstream-backed operations and prefer real healthCheck for safe read-only probes.",
2035
+ remediation: `Replace generated starter operation(s) ${generatedStarter.join(", ")} with real upstream-backed operations and add \`healthCheck\` for safe read-only probes.`,
2128
2036
  evidence: generatedStarter,
2129
2037
  };
2130
2038
  }
@@ -2137,13 +2045,9 @@ function scoreHealthCoverage(provider: ProviderDefinition): SubmitCheck {
2137
2045
  status: "warn",
2138
2046
  points: 12,
2139
2047
  maxPoints: CATEGORY_MAX_POINTS.health,
2140
- message:
2141
- "Health coverage is declared, with one or more unsupported probes.",
2142
- remediation:
2143
- "Reviewers prefer real healthCheck for safe read-only upstream operations.",
2144
- evidence: unsupported.map(
2145
- (operationId) => `${operationId}: healthCheckUnsupported`,
2146
- ),
2048
+ message: "Health coverage is declared, with one or more unsupported probes.",
2049
+ remediation: `For ${unsupported.join(", ")}, replace \`healthCheckUnsupported\` with \`healthCheck: { interval, cases }\` when the upstream operation is safe and read-only; keep unsupported only for destructive, paid, credential-sensitive, or flaky probes with a specific reason.`,
2050
+ evidence: unsupported.map((operationId) => `${operationId}: healthCheckUnsupported`),
2147
2051
  };
2148
2052
  }
2149
2053
 
@@ -2155,8 +2059,55 @@ function scoreHealthCoverage(provider: ProviderDefinition): SubmitCheck {
2155
2059
  );
2156
2060
  }
2157
2061
 
2158
- function scoreSmokeEvidence(smokeNote: string | undefined): SubmitCheck {
2159
- if (smokeNote?.trim()) {
2062
+ function scoreSmoke(
2063
+ smokeResult: SmokeResult | undefined,
2064
+ smokeNote: string | undefined,
2065
+ ): SubmitCheck {
2066
+ const deprecatedEvidence = smokeNote?.trim()
2067
+ ? ["Deprecated --smoke-note was provided and ignored for scoring."]
2068
+ : [];
2069
+ if (!smokeResult) {
2070
+ return {
2071
+ id: "local-smoke",
2072
+ category: "smoke",
2073
+ level: "warn",
2074
+ status: "warn",
2075
+ points: 0,
2076
+ maxPoints: CATEGORY_MAX_POINTS.smoke,
2077
+ message: "Measured local smoke was not run.",
2078
+ remediation:
2079
+ "Rerun submit-check with `--smoke` so it boots the provider, verifies `/health`, and POSTs every operation fixture. Set APIFUSE__PROVIDER__* env vars when live upstream credentials are available.",
2080
+ evidence: deprecatedEvidence,
2081
+ };
2082
+ }
2083
+
2084
+ const evidence = [
2085
+ `/health: ${smokeResult.healthOk ? "ok" : "failed"}`,
2086
+ ...smokeResult.operations.map(
2087
+ (outcome) =>
2088
+ `${outcome.operationId}: ${outcome.status}${outcome.httpStatus ? ` HTTP ${outcome.httpStatus}` : ""} - ${outcome.message}`,
2089
+ ),
2090
+ ...deprecatedEvidence,
2091
+ ];
2092
+ const incoherent = smokeResult.operations.filter((outcome) => outcome.status === "incoherent");
2093
+ if (!smokeResult.healthOk || smokeResult.bootError || incoherent.length > 0) {
2094
+ return {
2095
+ id: "local-smoke",
2096
+ category: "smoke",
2097
+ level: "blocker",
2098
+ status: "fail",
2099
+ points: 0,
2100
+ maxPoints: CATEGORY_MAX_POINTS.smoke,
2101
+ message: "Measured smoke failed to verify a coherent provider runtime.",
2102
+ remediation:
2103
+ "Fix the dev server boot, `/health`, or incoherent operation responses, then rerun `bun run submit-check -- --smoke`.",
2104
+ evidence: smokeResult.bootError ? [`boot: ${smokeResult.bootError}`, ...evidence] : evidence,
2105
+ details: smokeResult,
2106
+ };
2107
+ }
2108
+
2109
+ const successes = smokeResult.operations.filter((outcome) => outcome.status === "success");
2110
+ if (successes.length > 0) {
2160
2111
  return {
2161
2112
  id: "local-smoke",
2162
2113
  category: "smoke",
@@ -2164,8 +2115,9 @@ function scoreSmokeEvidence(smokeNote: string | undefined): SubmitCheck {
2164
2115
  status: "pass",
2165
2116
  points: CATEGORY_MAX_POINTS.smoke,
2166
2117
  maxPoints: CATEGORY_MAX_POINTS.smoke,
2167
- message: "Local smoke evidence was provided.",
2168
- evidence: [redact(smokeNote.trim())],
2118
+ message: "Measured smoke passed with at least one schema-valid operation success.",
2119
+ evidence,
2120
+ details: smokeResult,
2169
2121
  };
2170
2122
  }
2171
2123
 
@@ -2174,14 +2126,226 @@ function scoreSmokeEvidence(smokeNote: string | undefined): SubmitCheck {
2174
2126
  category: "smoke",
2175
2127
  level: "warn",
2176
2128
  status: "warn",
2177
- points: 5,
2129
+ points: 7,
2178
2130
  maxPoints: CATEGORY_MAX_POINTS.smoke,
2179
- message: "No local smoke evidence was provided.",
2131
+ message: "Runtime path was verified, but no live upstream schema-valid success was observed.",
2180
2132
  remediation:
2181
- "Start `bun run dev`, call `/health` and at least one `POST /v1/{operation}`, then rerun with `--smoke-note` or paste notes in the assigned workspace PR.",
2133
+ "Provide APIFUSE__PROVIDER__* env vars or fixture-safe upstream access, then rerun `bun run submit-check -- --smoke` to capture at least one schema-valid success.",
2134
+ evidence,
2135
+ details: smokeResult,
2182
2136
  };
2183
2137
  }
2184
2138
 
2139
+ export async function runSubmitCheckSmoke(
2140
+ providerRoot: string,
2141
+ provider?: ProviderDefinition,
2142
+ ): Promise<SmokeResult> {
2143
+ const loadedProvider = provider ?? (await loadProvider(providerRoot));
2144
+ if (!loadedProvider) {
2145
+ return {
2146
+ measured: true,
2147
+ healthOk: false,
2148
+ bootError: "Provider could not be loaded.",
2149
+ operations: [],
2150
+ };
2151
+ }
2152
+
2153
+ const port = await getAvailablePort();
2154
+ const server = spawn("bun", ["run", "dev"], {
2155
+ cwd: providerRoot,
2156
+ env: { ...process.env, APIFUSE__RUNTIME__PORT: String(port) },
2157
+ detached: process.platform !== "win32",
2158
+ stdio: ["ignore", "pipe", "pipe"],
2159
+ });
2160
+ let output = "";
2161
+ server.stdout?.on("data", (chunk) => {
2162
+ output += chunk.toString();
2163
+ });
2164
+ server.stderr?.on("data", (chunk) => {
2165
+ output += chunk.toString();
2166
+ });
2167
+
2168
+ try {
2169
+ const baseUrl = `http://127.0.0.1:${port}`;
2170
+ const health = await waitForSmokeHealth(`${baseUrl}/health`, server, () => output);
2171
+ if (!health.ok) {
2172
+ return {
2173
+ measured: true,
2174
+ healthOk: false,
2175
+ bootError: health.error,
2176
+ operations: [],
2177
+ };
2178
+ }
2179
+ const operations: SmokeOperationOutcome[] = [];
2180
+ for (const [operationId, operation] of Object.entries(loadedProvider.operations)) {
2181
+ operations.push(
2182
+ await smokeOperation(baseUrl, operationId, operation.output, {
2183
+ requestId: `req_submit_check_smoke_${operationId}`,
2184
+ input: operation.fixtures?.request ?? {},
2185
+ headers: {},
2186
+ }),
2187
+ );
2188
+ }
2189
+ return { measured: true, healthOk: true, operations };
2190
+ } finally {
2191
+ await stopSmokeServer(server);
2192
+ }
2193
+ }
2194
+
2195
+ async function smokeOperation(
2196
+ baseUrl: string,
2197
+ operationId: string,
2198
+ outputSchema: ProviderDefinition["operations"][string]["output"],
2199
+ body: unknown,
2200
+ ): Promise<SmokeOperationOutcome> {
2201
+ try {
2202
+ const response = await fetch(`${baseUrl}/v1/${operationId}`, {
2203
+ method: "POST",
2204
+ headers: { "content-type": "application/json" },
2205
+ body: JSON.stringify(body),
2206
+ signal: AbortSignal.timeout(20_000),
2207
+ });
2208
+ const payload = await response.json().catch(() => undefined);
2209
+ if (response.ok && isRecord(payload) && "data" in payload) {
2210
+ const parsed = safeParseSchemaSync(
2211
+ outputSchema,
2212
+ payload.data,
2213
+ `operations.${operationId}.output`,
2214
+ );
2215
+ if (parsed.success) {
2216
+ return {
2217
+ operationId,
2218
+ status: "success",
2219
+ httpStatus: response.status,
2220
+ message: "schema-valid success",
2221
+ };
2222
+ }
2223
+ return {
2224
+ operationId,
2225
+ status: "incoherent",
2226
+ httpStatus: response.status,
2227
+ message: "success payload failed output schema validation",
2228
+ };
2229
+ }
2230
+ if (isStructuredProviderError(payload) && response.status < 500) {
2231
+ return {
2232
+ operationId,
2233
+ status: "structured_error",
2234
+ httpStatus: response.status,
2235
+ message: `${payload.error.code}: ${payload.error.message}`,
2236
+ };
2237
+ }
2238
+ return {
2239
+ operationId,
2240
+ status: "incoherent",
2241
+ httpStatus: response.status,
2242
+ message: isStructuredProviderError(payload)
2243
+ ? `${payload.error.code}: ${payload.error.message}`
2244
+ : "response was not a schema-valid success or structured provider error",
2245
+ };
2246
+ } catch (error) {
2247
+ return {
2248
+ operationId,
2249
+ status: "incoherent",
2250
+ message: error instanceof Error ? error.message : String(error),
2251
+ };
2252
+ }
2253
+ }
2254
+
2255
+ function isStructuredProviderError(
2256
+ value: unknown,
2257
+ ): value is { error: { code: string; message: string } } {
2258
+ return (
2259
+ isRecord(value) &&
2260
+ isRecord(value.error) &&
2261
+ typeof value.error.code === "string" &&
2262
+ typeof value.error.message === "string"
2263
+ );
2264
+ }
2265
+
2266
+ async function getAvailablePort(): Promise<number> {
2267
+ return await new Promise((resolvePromise, rejectPromise) => {
2268
+ const server = createServer();
2269
+ server.once("error", rejectPromise);
2270
+ server.listen(0, "127.0.0.1", () => {
2271
+ const address = server.address();
2272
+ server.close((error) => {
2273
+ if (error) {
2274
+ rejectPromise(error);
2275
+ return;
2276
+ }
2277
+ if (!address || typeof address === "string") {
2278
+ rejectPromise(new Error("Could not allocate a local TCP port."));
2279
+ return;
2280
+ }
2281
+ resolvePromise(address.port);
2282
+ });
2283
+ });
2284
+ });
2285
+ }
2286
+
2287
+ async function waitForSmokeHealth(
2288
+ url: string,
2289
+ server: ChildProcess,
2290
+ getOutput: () => string,
2291
+ ): Promise<{ ok: true } | { ok: false; error: string }> {
2292
+ const deadline = Date.now() + 20_000;
2293
+ let lastError: unknown;
2294
+
2295
+ while (Date.now() < deadline) {
2296
+ if (server.exitCode !== null) {
2297
+ return {
2298
+ ok: false,
2299
+ error: `Dev server exited early with code ${server.exitCode}. ${getOutput()}`,
2300
+ };
2301
+ }
2302
+
2303
+ try {
2304
+ const response = await fetch(url, { signal: AbortSignal.timeout(1_000) });
2305
+ if (response.ok) return { ok: true };
2306
+ lastError = new Error(`${url} returned ${response.status}`);
2307
+ } catch (error) {
2308
+ lastError = error;
2309
+ }
2310
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 200));
2311
+ }
2312
+
2313
+ return {
2314
+ ok: false,
2315
+ error: `Timed out waiting for ${url}: ${lastError instanceof Error ? lastError.message : String(lastError)}. ${getOutput()}`,
2316
+ };
2317
+ }
2318
+
2319
+ async function stopSmokeServer(server: ChildProcess): Promise<void> {
2320
+ if (server.exitCode !== null) return;
2321
+ killSmokeProcessTree(server, "SIGTERM");
2322
+ await new Promise<void>((resolvePromise) => {
2323
+ const timeout = setTimeout(() => {
2324
+ if (server.exitCode === null) {
2325
+ killSmokeProcessTree(server, "SIGKILL");
2326
+ }
2327
+ resolvePromise();
2328
+ }, 2_000);
2329
+ server.once("exit", () => {
2330
+ clearTimeout(timeout);
2331
+ resolvePromise();
2332
+ });
2333
+ });
2334
+ }
2335
+
2336
+ function killSmokeProcessTree(server: ChildProcess, signal: NodeJS.Signals): void {
2337
+ if (server.pid === undefined) return;
2338
+ try {
2339
+ if (process.platform === "win32") {
2340
+ server.kill(signal);
2341
+ return;
2342
+ }
2343
+ process.kill(-server.pid, signal);
2344
+ } catch {
2345
+ server.kill(signal);
2346
+ }
2347
+ }
2348
+
2185
2349
  function scoreAuthSafety(provider: ProviderDefinition): SubmitCheck {
2186
2350
  const authMode = provider.auth?.mode ?? "none";
2187
2351
  const credentialKeys = provider.credential?.keys ?? [];
@@ -2205,7 +2369,7 @@ function scoreAuthSafety(provider: ProviderDefinition): SubmitCheck {
2205
2369
  maxPoints: CATEGORY_MAX_POINTS.auth,
2206
2370
  message: "OAuth auth mode does not declare persisted credential.keys.",
2207
2371
  remediation:
2208
- "Generated OAuth starters may begin without keys, but bounty-ready OAuth providers should declare persisted token keys once the real token exchange is implemented.",
2372
+ "Add `credential: { keys: [...] }` to `defineProvider` with the persisted OAuth token fields returned by the real token exchange.",
2209
2373
  };
2210
2374
  }
2211
2375
 
@@ -2221,10 +2385,8 @@ function scoreAuthSafety(provider: ProviderDefinition): SubmitCheck {
2221
2385
  status: "warn",
2222
2386
  points: 7,
2223
2387
  maxPoints: CATEGORY_MAX_POINTS.auth,
2224
- message:
2225
- "Provider is no-auth but at least one operation is not marked openWorld.",
2226
- remediation:
2227
- "Confirm auth.mode and operation annotations match the actual upstream auth model.",
2388
+ message: "Provider is no-auth but at least one operation is not marked openWorld.",
2389
+ remediation: `Either set \`auth.mode\` to the upstream auth model, or mark these public no-auth operations with \`annotations.openWorld: true\`: ${securedOperations.map(([operationId]) => operationId).join(", ")}.`,
2228
2390
  evidence: securedOperations.map(([operationId]) => operationId),
2229
2391
  };
2230
2392
  }
@@ -2266,9 +2428,7 @@ function scoreProviderDocs(providerRoot: string): SubmitCheck[] {
2266
2428
 
2267
2429
  const points = Math.max(
2268
2430
  0,
2269
- CATEGORY_MAX_POINTS.docs -
2270
- missing.length * 2 -
2271
- (mentionsSubmitCheck ? 0 : 1),
2431
+ CATEGORY_MAX_POINTS.docs - missing.length * 2 - (mentionsSubmitCheck ? 0 : 1),
2272
2432
  );
2273
2433
 
2274
2434
  return [
@@ -2285,7 +2445,7 @@ function scoreProviderDocs(providerRoot: string): SubmitCheck[] {
2285
2445
  : "Provider README includes expected submission guidance.",
2286
2446
  remediation:
2287
2447
  missing.length > 0 || !mentionsSubmitCheck
2288
- ? "Include Parameters, Response, Example, and submit-check evidence guidance."
2448
+ ? "Update `README.md` to include `Parameters`, `Response`, `Example`, and submit-check evidence guidance sections."
2289
2449
  : undefined,
2290
2450
  evidence: [
2291
2451
  ...missing.map(([, label]) => `missing ${label}`),
@@ -2295,9 +2455,10 @@ function scoreProviderDocs(providerRoot: string): SubmitCheck[] {
2295
2455
  ];
2296
2456
  }
2297
2457
 
2298
- function scoreSecrets(providerRoot: string): SubmitCheck {
2299
- const findings = findSecretFindings(providerRoot);
2300
- if (findings.length > 0) {
2458
+ function scoreSecrets(providerRoot: string, provider?: ProviderDefinition): SubmitCheck {
2459
+ const findings = findSecretFindings(providerRoot, provider?.id);
2460
+ const blockerFindings = findings.filter((finding) => finding.level !== "warn");
2461
+ if (blockerFindings.length > 0) {
2301
2462
  return {
2302
2463
  id: "secret-scan",
2303
2464
  category: "security",
@@ -2305,11 +2466,34 @@ function scoreSecrets(providerRoot: string): SubmitCheck {
2305
2466
  status: "fail",
2306
2467
  points: 0,
2307
2468
  maxPoints: CATEGORY_MAX_POINTS.security,
2469
+ message: "Potential real credential material was found in shareable files.",
2470
+ remediation:
2471
+ blockerFindings[0]?.remediation ??
2472
+ 'Move hardcoded credentials to env vars read via `ctx.env.get("APIFUSE__PROVIDER__<ID>__<NAME>")` and rotate the leaked credential.',
2473
+ evidence: blockerFindings.map(
2474
+ (finding) =>
2475
+ finding.evidence ??
2476
+ `${finding.file}${finding.line ? `:${finding.line}` : ""}: ${finding.label}`,
2477
+ ),
2478
+ };
2479
+ }
2480
+ if (findings.length > 0) {
2481
+ return {
2482
+ id: "secret-scan",
2483
+ category: "security",
2484
+ level: "warn",
2485
+ status: "warn",
2486
+ points: 8,
2487
+ maxPoints: CATEGORY_MAX_POINTS.security,
2308
2488
  message:
2309
- "Potential real credential material was found in shareable files.",
2489
+ "High-entropy source strings were found without secret-like identifier context; they may be false positives.",
2310
2490
  remediation:
2311
- "Remove real secrets from source, README, and fixtures. Use environment variables and local-only connection.secrets instead.",
2312
- evidence: findings.map((finding) => `${finding.file}: ${finding.label}`),
2491
+ 'Review the listed strings. If any are credentials, move them to env vars read via `ctx.env.get("APIFUSE__PROVIDER__<ID>__<NAME>")` and rotate the leaked credential; otherwise keep generated blobs in fixtures/tests or document why they are public.',
2492
+ evidence: findings.map(
2493
+ (finding) =>
2494
+ finding.evidence ??
2495
+ `${finding.file}${finding.line ? `:${finding.line}` : ""}: ${finding.label}`,
2496
+ ),
2313
2497
  };
2314
2498
  }
2315
2499
 
@@ -2321,7 +2505,7 @@ function scoreSecrets(providerRoot: string): SubmitCheck {
2321
2505
  );
2322
2506
  }
2323
2507
 
2324
- function findSecretFindings(providerRoot: string): SecretFinding[] {
2508
+ function findSecretFindings(providerRoot: string, providerId = "<ID>"): SecretFinding[] {
2325
2509
  const candidateFiles = [
2326
2510
  "README.md",
2327
2511
  "index.ts",
@@ -2342,14 +2526,155 @@ function findSecretFindings(providerRoot: string): SecretFinding[] {
2342
2526
  }
2343
2527
  }
2344
2528
 
2529
+ findings.push(...findEntropySecretFindings(providerRoot, providerId));
2345
2530
  return findings;
2346
2531
  }
2347
2532
 
2533
+ function findEntropySecretFindings(providerRoot: string, providerId: string): SecretFinding[] {
2534
+ const findings: SecretFinding[] = [];
2535
+ for (const filePath of listNonTestProviderSourceFiles(providerRoot)) {
2536
+ const relativePath = toRelativeProviderPath(providerRoot, filePath);
2537
+ if (isEntropySecretExcludedPath(relativePath)) continue;
2538
+ const content = readFileSync(filePath, "utf8");
2539
+ const lines = content.split(/\r?\n/);
2540
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
2541
+ const line = lines[lineIndex] ?? "";
2542
+ for (const candidate of extractStringLiteralCandidates(line)) {
2543
+ const finding = classifyEntropyCandidate({
2544
+ value: candidate,
2545
+ line,
2546
+ file: relativePath,
2547
+ lineNumber: lineIndex + 1,
2548
+ providerId,
2549
+ });
2550
+ if (finding) findings.push(finding);
2551
+ }
2552
+ }
2553
+ }
2554
+ return findings;
2555
+ }
2556
+
2557
+ function isEntropySecretExcludedPath(relativePath: string): boolean {
2558
+ return (
2559
+ relativePath.endsWith(".test.ts") ||
2560
+ relativePath.startsWith("__tests__/") ||
2561
+ relativePath.includes("/__tests__/") ||
2562
+ relativePath.startsWith("__fixtures__/") ||
2563
+ relativePath.includes("/__fixtures__/")
2564
+ );
2565
+ }
2566
+
2567
+ export function extractStringLiteralCandidates(line: string): string[] {
2568
+ const candidates: string[] = [];
2569
+ for (let index = 0; index < line.length; index += 1) {
2570
+ const quote = line[index];
2571
+ if (quote !== '"' && quote !== "'" && quote !== "`") continue;
2572
+
2573
+ const contentStart = index + 1;
2574
+ let cursor = contentStart;
2575
+ while (cursor < line.length) {
2576
+ const char = line[cursor];
2577
+ if (char === "\\") {
2578
+ cursor += 2;
2579
+ continue;
2580
+ }
2581
+ if (char === quote) {
2582
+ if (cursor - contentStart >= 20) {
2583
+ candidates.push(line.slice(contentStart, cursor));
2584
+ }
2585
+ index = cursor;
2586
+ break;
2587
+ }
2588
+ cursor += 1;
2589
+ }
2590
+ }
2591
+ return candidates;
2592
+ }
2593
+
2594
+ function classifyEntropyCandidate(input: {
2595
+ value: string;
2596
+ line: string;
2597
+ file: string;
2598
+ lineNumber: number;
2599
+ providerId: string;
2600
+ }): SecretFinding | undefined {
2601
+ const value = input.value;
2602
+ if (!shouldConsiderEntropyValue(value)) return undefined;
2603
+ const charset = classifyEntropyCharset(value);
2604
+ if (!charset) return undefined;
2605
+ const entropy = shannonEntropy(value);
2606
+ const secretishContext = SECRETISH_IDENTIFIER_PATTERN.test(input.line);
2607
+ const threshold = charset === "hex" ? 3.0 : secretishContext ? 4.0 : 4.5;
2608
+ if (entropy < threshold) return undefined;
2609
+
2610
+ const preview = `${value.slice(0, 4)}...[REDACTED length=${value.length}]`;
2611
+ const envName = `APIFUSE__PROVIDER__${input.providerId.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}__${guessSecretName(input.line)}`;
2612
+ const location = `${input.file}:${input.lineNumber}`;
2613
+ const label =
2614
+ charset === "hex"
2615
+ ? `high-entropy hex string (${entropy.toFixed(2)} bits/char)`
2616
+ : `high-entropy base64-like string (${entropy.toFixed(2)} bits/char)`;
2617
+ return {
2618
+ label,
2619
+ file: input.file,
2620
+ line: input.lineNumber,
2621
+ level: secretishContext ? "blocker" : "warn",
2622
+ remediation: `Move ${location} to an env var read via \`ctx.env.get("${envName}")\` and rotate the leaked credential.`,
2623
+ evidence: `${location}: ${label}; preview ${preview}${secretishContext ? "" : "; may be a false positive"}`,
2624
+ };
2625
+ }
2626
+
2627
+ function shouldConsiderEntropyValue(value: string): boolean {
2628
+ const lower = value.toLowerCase();
2629
+ if (/^(?:dev-only|local|example|sample|your-|replace|<)/i.test(value)) {
2630
+ return false;
2631
+ }
2632
+ if (/^sha(?:256|512)-/i.test(value)) return false;
2633
+ if (/\s/.test(value)) return false;
2634
+ if (value.includes("${")) return false;
2635
+ if (value.includes("/")) return false;
2636
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) return false;
2637
+ if (/^(?:\.{0,2}\/|~\/|[A-Za-z]:\\)/.test(value)) return false;
2638
+ if (value.includes(".") && /^[A-Za-z0-9_.-]+$/.test(value)) return false;
2639
+ if (lower.includes("/") && /\.[a-z0-9]{1,8}(?:$|[/?#])/i.test(value)) {
2640
+ return false;
2641
+ }
2642
+ return value.length >= 20;
2643
+ }
2644
+
2645
+ function classifyEntropyCharset(value: string): "base64" | "hex" | undefined {
2646
+ if (/^[a-f0-9]+$/i.test(value) && value.length >= 32) return "hex";
2647
+ const base64ishChars = value.match(/[A-Za-z0-9+/=_-]/g)?.length ?? 0;
2648
+ if (base64ishChars / value.length >= 0.9) return "base64";
2649
+ return undefined;
2650
+ }
2651
+
2652
+ function shannonEntropy(value: string): number {
2653
+ const counts = new Map<string, number>();
2654
+ for (const char of value) counts.set(char, (counts.get(char) ?? 0) + 1);
2655
+ let entropy = 0;
2656
+ for (const count of counts.values()) {
2657
+ const probability = count / value.length;
2658
+ entropy -= probability * Math.log2(probability);
2659
+ }
2660
+ return entropy;
2661
+ }
2662
+
2663
+ function guessSecretName(line: string): string {
2664
+ const match =
2665
+ /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)/.exec(line) ??
2666
+ /["']?([A-Za-z_$][\w$-]*)["']?\s*:/.exec(line);
2667
+ const raw = match?.[1] ?? "SECRET";
2668
+ return raw
2669
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
2670
+ .toUpperCase()
2671
+ .replace(/[^A-Z0-9]+/g, "_");
2672
+ }
2673
+
2674
+ const SECRETISH_IDENTIFIER_PATTERN = /key|token|secret|password|credential|auth/i;
2675
+
2348
2676
  const SECRET_PATTERNS: Array<[string, RegExp]> = [
2349
- [
2350
- "JWT-like token",
2351
- /eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}/,
2352
- ],
2677
+ ["JWT-like token", /eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}/],
2353
2678
  ["GitHub token", /gh[pousr]_[A-Za-z0-9_]{30,}/],
2354
2679
  ["Stripe live key", /(?:sk|rk)_live_[A-Za-z0-9]{20,}/],
2355
2680
  ["Bearer token", /Bearer\s+[A-Za-z0-9._~+/=-]{32,}/i],
@@ -2359,9 +2684,7 @@ const SECRET_PATTERNS: Array<[string, RegExp]> = [
2359
2684
  ],
2360
2685
  ];
2361
2686
 
2362
- async function safeLoadProvider(
2363
- providerRoot: string,
2364
- ): Promise<ProviderDefinition | undefined> {
2687
+ async function safeLoadProvider(providerRoot: string): Promise<ProviderDefinition | undefined> {
2365
2688
  try {
2366
2689
  return await loadProvider(providerRoot);
2367
2690
  } catch {
@@ -2369,9 +2692,7 @@ async function safeLoadProvider(
2369
2692
  }
2370
2693
  }
2371
2694
 
2372
- async function loadProvider(
2373
- providerRoot: string,
2374
- ): Promise<ProviderDefinition | undefined> {
2695
+ async function loadProvider(providerRoot: string): Promise<ProviderDefinition | undefined> {
2375
2696
  const entryPath = resolve(providerRoot, "index.ts");
2376
2697
  if (!existsSync(entryPath)) {
2377
2698
  return undefined;
@@ -2451,8 +2772,7 @@ export function renderText(report: SubmitCheckReport): string {
2451
2772
  ];
2452
2773
 
2453
2774
  for (const check of report.checks) {
2454
- const marker =
2455
- check.status === "pass" ? "✓" : check.status === "warn" ? "⚠" : "✗";
2775
+ const marker = check.status === "pass" ? "✓" : check.status === "warn" ? "⚠" : "✗";
2456
2776
  lines.push(
2457
2777
  `${marker} [${check.category}] ${check.message} (${check.points}/${check.maxPoints})`,
2458
2778
  );
@@ -2474,9 +2794,7 @@ export function renderMarkdown(report: SubmitCheckReport): string {
2474
2794
  `- **Provider**: ${report.provider.id}@${report.provider.version}`,
2475
2795
  `- **SDK**: ${report.provider.sdkVersion}`,
2476
2796
  `- **Runtime/Auth**: ${report.provider.runtime} / ${report.provider.authMode}`,
2477
- ...(report.provider.tier
2478
- ? [`- **Bounty tier**: ${report.provider.tier}`]
2479
- : []),
2797
+ ...(report.provider.tier ? [`- **Bounty tier**: ${report.provider.tier}`] : []),
2480
2798
  `- **Score**: ${report.score.total}/${report.score.max}`,
2481
2799
  `- **Verdict**: ${report.score.verdict}`,
2482
2800
  `- **Blockers**: ${report.summary.blockers}`,
@@ -2489,12 +2807,7 @@ export function renderMarkdown(report: SubmitCheckReport): string {
2489
2807
  ];
2490
2808
 
2491
2809
  for (const check of report.checks) {
2492
- const status =
2493
- check.status === "pass"
2494
- ? "PASS"
2495
- : check.status === "warn"
2496
- ? "WARN"
2497
- : "FAIL";
2810
+ const status = check.status === "pass" ? "PASS" : check.status === "warn" ? "WARN" : "FAIL";
2498
2811
  lines.push(
2499
2812
  `| ${status} | ${escapeMarkdown(check.category)} | ${escapeMarkdown(check.message)} | ${check.points}/${check.maxPoints} | ${escapeMarkdown(check.remediation ?? "")} |`,
2500
2813
  );
@@ -2524,9 +2837,7 @@ function redact(value: string): string {
2524
2837
  }
2525
2838
 
2526
2839
  function toGlobalRegex(pattern: RegExp): RegExp {
2527
- return pattern.global
2528
- ? pattern
2529
- : new RegExp(pattern.source, `${pattern.flags}g`);
2840
+ return pattern.global ? pattern : new RegExp(pattern.source, `${pattern.flags}g`);
2530
2841
  }
2531
2842
 
2532
2843
  function clamp(value: number, min: number, max: number): number {