@apifuse/provider-sdk 2.1.0-beta.3 → 2.1.0-beta.5

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.
Files changed (63) hide show
  1. package/AUTHORING.md +187 -8
  2. package/CHANGELOG.md +13 -1
  3. package/README.md +40 -18
  4. package/SUBMISSION.md +4 -4
  5. package/bin/apifuse-dev.ts +12 -5
  6. package/bin/apifuse-pack-check.ts +9 -2
  7. package/bin/apifuse-pack-smoke.ts +127 -6
  8. package/bin/apifuse-perf.ts +76 -31
  9. package/bin/apifuse-record.ts +148 -94
  10. package/bin/apifuse-submit-check.ts +243 -7
  11. package/bin/apifuse.ts +1 -1
  12. package/package.json +17 -8
  13. package/src/choice-token.ts +164 -0
  14. package/src/cli/commands.ts +4 -7
  15. package/src/cli/create.ts +180 -51
  16. package/src/cli/templates/provider/.dockerignore.tpl +22 -0
  17. package/src/cli/templates/provider/.gitignore.tpl +22 -0
  18. package/src/cli/templates/provider/README.md.tpl +42 -7
  19. package/src/cli/templates/provider/dev.ts.tpl +1 -1
  20. package/src/cli/templates/provider/domain/README.md.tpl +3 -0
  21. package/src/cli/templates/provider/index.ts.tpl +5 -47
  22. package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
  23. package/src/cli/templates/provider/meta.ts.tpl +7 -0
  24. package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
  25. package/src/cli/templates/provider/operations/ping.ts.tpl +23 -0
  26. package/src/cli/templates/provider/schemas/ping.ts.tpl +16 -0
  27. package/src/cli/templates/provider/start.ts.tpl +1 -1
  28. package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
  29. package/src/config/loader.ts +1206 -9
  30. package/src/define.ts +1620 -106
  31. package/src/errors.ts +12 -0
  32. package/src/i18n/catalog.ts +121 -0
  33. package/src/i18n/index.ts +2 -0
  34. package/src/i18n/keys.ts +64 -0
  35. package/src/index.ts +149 -8
  36. package/src/lint.ts +306 -51
  37. package/src/observability.ts +41 -0
  38. package/src/provider.ts +60 -3
  39. package/src/public-schema-field-lint.ts +237 -0
  40. package/src/runtime/auth-flow.ts +7 -0
  41. package/src/runtime/browser.ts +77 -21
  42. package/src/runtime/cache.ts +582 -0
  43. package/src/runtime/executor.ts +13 -1
  44. package/src/runtime/http.ts +939 -195
  45. package/src/runtime/insights.ts +11 -11
  46. package/src/runtime/instrumentation.ts +12 -4
  47. package/src/runtime/key-derivation.ts +1 -1
  48. package/src/runtime/keyring.ts +4 -3
  49. package/src/runtime/proxy-errors.ts +132 -0
  50. package/src/runtime/proxy-telemetry.ts +253 -0
  51. package/src/runtime/request-options.ts +66 -0
  52. package/src/runtime/state.ts +76 -0
  53. package/src/runtime/stealth.ts +1145 -0
  54. package/src/runtime/stt.ts +629 -0
  55. package/src/runtime/trace.ts +1 -1
  56. package/src/schema.ts +363 -1
  57. package/src/server/serve.ts +816 -58
  58. package/src/server/types.ts +35 -0
  59. package/src/stream.ts +210 -0
  60. package/src/testing/run.ts +17 -4
  61. package/src/types.ts +876 -53
  62. package/src/runtime/tls.ts +0 -434
  63. package/src/types/playwright-stealth.d.ts +0 -9
@@ -7,13 +7,18 @@ import { basename, dirname, relative, resolve } from "node:path";
7
7
  import { pathToFileURL } from "node:url";
8
8
 
9
9
  import {
10
+ createBypassProviderCache,
10
11
  createHttpClient,
11
- createTlsClient,
12
+ createStealthClient,
13
+ createSttClientFromEnv,
12
14
  executeOperation,
13
15
  type HttpClient,
14
16
  type ProviderContext,
15
17
  type ProviderDefinition,
16
- type TlsClient,
18
+ ProviderError,
19
+ type StealthClient,
20
+ TransportError,
21
+ ValidationError,
17
22
  } from "../src";
18
23
 
19
24
  type CliArgs = {
@@ -28,55 +33,77 @@ type ProviderRuntime = ProviderDefinition;
28
33
 
29
34
  type MutableRecord = Record<string, unknown>;
30
35
 
31
- export async function main() {
32
- const args = parseArgs(normalizeArgs(process.argv.slice(2)));
33
- const location = resolveProviderLocation(args.providerPath);
34
- const provider = await loadProvider(location.rootDir);
35
- const operationName = resolveOperationName(provider, args.operation);
36
- const operation = provider.operations[operationName];
37
- const parsedParams = parseParams(operation, args.params);
38
-
39
- const capture = createCaptureContext(
40
- resolveOperationBaseUrl(provider, operationName),
41
- );
36
+ const HELP_TEXT = `Usage: apifuse record [path] --operation <operation> --params '<json>'
42
37
 
43
- console.log(`[apifuse record] Calling ${operationName} on ${provider.id}...`);
38
+ Calls a real upstream-backed operation through ctx.http or ctx.stealth and writes __fixtures__/raw.json.
44
39
 
45
- const result = await executeOperation(
46
- provider,
47
- operationName,
48
- capture.ctx,
49
- parsedParams,
50
- );
51
- const captured = capture.getCapturedRaw();
40
+ Options:
41
+ --operation, -o <name> operation to call
42
+ --params, -p <json> JSON input passed to the operation (default: {})
43
+ --append append to an existing array fixture
44
+ --sanitize redact common token/header fields (default)
45
+ --no-sanitize write the captured upstream payload as-is
46
+ --help, -h show this help
52
47
 
53
- if (captured === undefined) {
54
- throw new Error(
55
- `No upstream response was captured for ${provider.id}.${operationName}.`,
48
+ Example:
49
+ apifuse record providers/airkorea --operation realtime --params '{"stationName":"jongno"}'`;
50
+
51
+ export async function main() {
52
+ try {
53
+ const args = parseArgs(normalizeArgs(process.argv.slice(2)));
54
+ const location = resolveProviderLocation(args.providerPath);
55
+ const provider = await loadProvider(location.rootDir);
56
+ const operationName = resolveOperationName(provider, args.operation);
57
+ const operation = provider.operations[operationName];
58
+ const parsedParams = parseParams(operation, args.params);
59
+
60
+ const capture = createCaptureContext(
61
+ provider,
62
+ resolveOperationBaseUrl(provider, operationName),
56
63
  );
57
- }
58
64
 
59
- const rawPayload = args.sanitize ? sanitizeFixture(captured) : captured;
60
- const fixturePath = resolve(location.rootDir, "__fixtures__", "raw.json");
61
- const nextPayload = await prepareFixturePayload(
62
- fixturePath,
63
- rawPayload,
64
- args.append,
65
- );
65
+ console.log(
66
+ `[apifuse record] Calling ${operationName} on ${provider.id}...`,
67
+ );
66
68
 
67
- await mkdir(dirname(fixturePath), { recursive: true });
68
- await writeFile(fixturePath, `${JSON.stringify(nextPayload, null, 2)}\n`);
69
+ const result = await executeOperation(
70
+ provider,
71
+ operationName,
72
+ capture.ctx,
73
+ parsedParams,
74
+ );
75
+ const captured = capture.getCapturedRaw();
69
76
 
70
- console.log(
71
- `[apifuse record] Captured response (${formatBytes(
72
- Buffer.byteLength(JSON.stringify(rawPayload)),
73
- )})`,
74
- );
75
- console.log(
76
- `[apifuse record] Saved to ${relative(process.cwd(), fixturePath)}`,
77
- );
77
+ if (captured === undefined) {
78
+ throw new Error(
79
+ `No upstream response was captured for ${provider.id}.${operationName}.`,
80
+ );
81
+ }
82
+
83
+ const rawPayload = args.sanitize ? sanitizeFixture(captured) : captured;
84
+ const fixturePath = resolve(location.rootDir, "__fixtures__", "raw.json");
85
+ const nextPayload = await prepareFixturePayload(
86
+ fixturePath,
87
+ rawPayload,
88
+ args.append,
89
+ );
90
+
91
+ await mkdir(dirname(fixturePath), { recursive: true });
92
+ await writeFile(fixturePath, `${JSON.stringify(nextPayload, null, 2)}\n`);
93
+
94
+ console.log(
95
+ `[apifuse record] Captured response (${formatBytes(
96
+ Buffer.byteLength(JSON.stringify(rawPayload)),
97
+ )})`,
98
+ );
99
+ console.log(
100
+ `[apifuse record] Saved to ${relative(process.cwd(), fixturePath)}`,
101
+ );
78
102
 
79
- void result;
103
+ void result;
104
+ } catch (error) {
105
+ handleCliError(error);
106
+ }
80
107
  }
81
108
 
82
109
  function normalizeArgs(argv: string[]): string[] {
@@ -104,6 +131,11 @@ function parseArgs(argv: string[]): CliArgs {
104
131
  continue;
105
132
  }
106
133
 
134
+ if (arg === "--help" || arg === "-h") {
135
+ console.log(HELP_TEXT);
136
+ process.exit(0);
137
+ }
138
+
107
139
  if (arg.startsWith("--operation=")) {
108
140
  operation = arg.slice("--operation=".length);
109
141
  continue;
@@ -155,6 +187,43 @@ function parseArgs(argv: string[]): CliArgs {
155
187
  return { append, providerPath, operation, params, sanitize };
156
188
  }
157
189
 
190
+ function handleCliError(error: unknown): never {
191
+ const message = formatCliError(error);
192
+ console.error(`[apifuse record] ${message}`);
193
+ process.exit(1);
194
+ }
195
+
196
+ function formatCliError(error: unknown): string {
197
+ if (error instanceof TransportError) {
198
+ return [
199
+ error.message,
200
+ error.upstreamStatus ? `status=${error.upstreamStatus}` : undefined,
201
+ error.options?.retryable !== undefined
202
+ ? `retryable=${String(error.options.retryable)}`
203
+ : undefined,
204
+ error.fix ? `fix=${error.fix}` : undefined,
205
+ ]
206
+ .filter(Boolean)
207
+ .join(" ");
208
+ }
209
+
210
+ if (error instanceof ProviderError || error instanceof ValidationError) {
211
+ return [
212
+ error.message,
213
+ error.code ? `code=${error.code}` : undefined,
214
+ error.fix,
215
+ ]
216
+ .filter(Boolean)
217
+ .join(" ");
218
+ }
219
+
220
+ if (error instanceof Error) {
221
+ return error.message;
222
+ }
223
+
224
+ return String(error);
225
+ }
226
+
158
227
  function resolveProviderLocation(inputPath?: string) {
159
228
  const originalInput = inputPath ?? process.cwd();
160
229
  const resolvedInput = resolve(process.cwd(), originalInput);
@@ -269,15 +338,18 @@ function resolveOperationBaseUrl(
269
338
  return baseUrl;
270
339
  }
271
340
 
272
- function createCaptureContext(baseUrl: string) {
341
+ function createCaptureContext(provider: ProviderRuntime, baseUrl: string) {
273
342
  let capturedRaw: unknown;
274
343
 
275
344
  const http = proxyHttpClient(createHttpClient(baseUrl), (response) => {
276
345
  capturedRaw = response.data;
277
346
  });
278
- const tls = proxyTlsClient(createTlsClient(baseUrl), (response) => {
279
- capturedRaw = normalizeCapturedTlsResponse(response);
280
- });
347
+ const stealth = proxyStealthClient(
348
+ createStealthClient(baseUrl),
349
+ (response) => {
350
+ capturedRaw = normalizeCapturedStealthResponse(response);
351
+ },
352
+ );
281
353
 
282
354
  const ctx: ProviderContext = {
283
355
  env: {
@@ -291,7 +363,8 @@ function createCaptureContext(baseUrl: string) {
291
363
  getScopes: () => [],
292
364
  },
293
365
  http,
294
- tls,
366
+ cache: createBypassProviderCache({ providerId: provider.id }),
367
+ stealth,
295
368
  browser: {
296
369
  engine: "playwright-stealth",
297
370
  newPage: async () => {
@@ -306,6 +379,7 @@ function createCaptureContext(baseUrl: string) {
306
379
  throw new Error("Auth prompts are not available in apifuse record.");
307
380
  },
308
381
  },
382
+ stt: createSttClientFromEnv(provider.stt),
309
383
  };
310
384
 
311
385
  return {
@@ -335,59 +409,39 @@ function proxyHttpClient(
335
409
  }) as HttpClient;
336
410
  }
337
411
 
338
- function proxyTlsClient(
339
- client: TlsClient,
340
- onResponse: (response: Awaited<ReturnType<TlsClient["fetch"]>>) => void,
341
- ): TlsClient {
342
- return new Proxy(client, {
343
- get(target, prop, receiver) {
344
- const value = Reflect.get(target, prop, receiver);
345
-
346
- if (prop === "fetch" && typeof value === "function") {
347
- return async (...args: unknown[]) => {
348
- const response = await value.apply(target, args);
349
- onResponse(response);
350
- return response;
351
- };
352
- }
353
-
354
- if (prop === "createSession" && typeof value === "function") {
355
- return (...args: unknown[]) => {
356
- const session = value.apply(target, args) as ReturnType<
357
- TlsClient["createSession"]
358
- >;
359
- return proxyTlsSession(session, onResponse);
360
- };
361
- }
412
+ type StealthSession = ReturnType<StealthClient["createSession"]>;
362
413
 
363
- return value;
414
+ function proxyStealthClient(
415
+ client: StealthClient,
416
+ onResponse: (response: Awaited<ReturnType<StealthClient["fetch"]>>) => void,
417
+ ): StealthClient {
418
+ return {
419
+ fetch: async (...args: Parameters<StealthClient["fetch"]>) => {
420
+ const response = await client.fetch(...args);
421
+ onResponse(response);
422
+ return response;
364
423
  },
365
- }) as TlsClient;
424
+ createSession: (...args: Parameters<StealthClient["createSession"]>) =>
425
+ proxyStealthSession(client.createSession(...args), onResponse),
426
+ };
366
427
  }
367
428
 
368
- function proxyTlsSession(
369
- session: ReturnType<TlsClient["createSession"]>,
370
- onResponse: (response: Awaited<ReturnType<TlsClient["fetch"]>>) => void,
371
- ) {
372
- return new Proxy(session, {
373
- get(target, prop, receiver) {
374
- const value = Reflect.get(target, prop, receiver);
375
-
376
- if (prop === "fetch" && typeof value === "function") {
377
- return async (...args: unknown[]) => {
378
- const response = await value.apply(target, args);
379
- onResponse(response);
380
- return response;
381
- };
382
- }
383
-
384
- return value;
429
+ function proxyStealthSession(
430
+ session: StealthSession,
431
+ onResponse: (response: Awaited<ReturnType<StealthClient["fetch"]>>) => void,
432
+ ): StealthSession {
433
+ return {
434
+ fetch: async (...args: Parameters<StealthSession["fetch"]>) => {
435
+ const response = await session.fetch(...args);
436
+ onResponse(response);
437
+ return response;
385
438
  },
386
- }) as ReturnType<TlsClient["createSession"]>;
439
+ close: () => session.close(),
440
+ };
387
441
  }
388
442
 
389
- function normalizeCapturedTlsResponse(
390
- response: Awaited<ReturnType<TlsClient["fetch"]>>,
443
+ function normalizeCapturedStealthResponse(
444
+ response: Awaited<ReturnType<StealthClient["fetch"]>>,
391
445
  ) {
392
446
  try {
393
447
  return JSON.parse(response.body);
@@ -2,11 +2,19 @@
2
2
 
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { writeFile } from "node:fs/promises";
5
- import { basename, dirname, resolve } from "node:path";
5
+ import { basename, dirname, join, resolve } from "node:path";
6
6
  import { pathToFileURL } from "node:url";
7
7
 
8
+ import { z } from "zod";
9
+
8
10
  import packageJson from "../package.json";
9
11
  import type { ProviderDefinition } from "../src";
12
+ import {
13
+ loadProviderLocaleCatalogs,
14
+ type ProviderLocale,
15
+ validateProviderLocaleCatalogs,
16
+ } from "../src/i18n";
17
+ import { APIFUSE_DESCRIPTION_KEY_META_KEY } from "../src/schema";
10
18
  import { type CheckResult, runChecks } from "./apifuse-check";
11
19
 
12
20
  const TIERS = ["bronze", "silver", "gold", "diamond"] as const;
@@ -77,6 +85,11 @@ const CATEGORY_MAX_POINTS = {
77
85
  docs: 10,
78
86
  } as const;
79
87
 
88
+ const REQUIRED_PUBLIC_PROVIDER_LOCALES = [
89
+ "en",
90
+ "ko",
91
+ ] as const satisfies readonly ProviderLocale[];
92
+
80
93
  const HELP_TEXT = `Usage: apifuse submit-check [path] [--tier bronze|silver|gold|diamond] [--json] [--markdown <path>] [--smoke-note <text>]
81
94
  Alias: apifuse bounty-check [path]
82
95
  Default: apifuse submit-check .`;
@@ -220,12 +233,14 @@ export async function buildSubmitCheckReport(
220
233
  checks.push(...scoreBaseChecks(baseChecks));
221
234
 
222
235
  if (provider) {
236
+ checks.push(scoreLocaleCatalog(providerRoot, provider));
223
237
  checks.push(scoreOperationMetadata(provider));
224
238
  checks.push(scoreFixtureCoverage(provider));
225
239
  checks.push(scoreHealthCoverage(provider));
226
240
  checks.push(scoreAuthSafety(provider));
227
241
  checks.push(scoreSmokeEvidence(args.smokeNote));
228
242
  checks.push(...scoreProviderDocs(providerRoot));
243
+ checks.push(scoreRepositoryDx(providerRoot));
229
244
  checks.push(scoreSecrets(providerRoot));
230
245
  } else {
231
246
  checks.push(
@@ -273,6 +288,69 @@ export async function buildSubmitCheckReport(
273
288
  };
274
289
  }
275
290
 
291
+ function scoreRepositoryDx(providerRoot: string): SubmitCheck {
292
+ const missing: string[] = [];
293
+ if (!existsSync(resolve(providerRoot, ".gitignore"))) {
294
+ missing.push(".gitignore");
295
+ }
296
+
297
+ const packageJsonPath = resolve(providerRoot, "package.json");
298
+ const packageScripts = readPackageScripts(packageJsonPath);
299
+ if (typeof packageScripts?.["type-check"] !== "string") {
300
+ missing.push("package.json scripts.type-check");
301
+ }
302
+ if (!checkScriptRunsTypeCheck(packageScripts?.check)) {
303
+ missing.push("package.json scripts.check includes type-check");
304
+ }
305
+
306
+ if (missing.length === 0) {
307
+ return pass(
308
+ "repository-dx",
309
+ "docs",
310
+ "Repository includes generated-provider DX guardrails.",
311
+ 0,
312
+ );
313
+ }
314
+
315
+ return {
316
+ id: "repository-dx",
317
+ category: "docs",
318
+ level: "warn",
319
+ status: "warn",
320
+ points: 0,
321
+ maxPoints: 0,
322
+ message: `Generated repository DX guardrails are missing: ${missing.join(", ")}.`,
323
+ remediation:
324
+ "Regenerate with the current `apifuse create` template or add .gitignore plus `type-check: tsc --noEmit` and include it from `check`.",
325
+ evidence: missing,
326
+ };
327
+ }
328
+
329
+ function readPackageScripts(
330
+ packageJsonPath: string,
331
+ ): Record<string, unknown> | undefined {
332
+ if (!existsSync(packageJsonPath)) {
333
+ return undefined;
334
+ }
335
+
336
+ try {
337
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
338
+ if (!isRecord(packageJson) || !isRecord(packageJson.scripts)) {
339
+ return undefined;
340
+ }
341
+ return packageJson.scripts;
342
+ } catch {
343
+ return undefined;
344
+ }
345
+ }
346
+
347
+ function checkScriptRunsTypeCheck(checkScript: unknown): boolean {
348
+ return (
349
+ typeof checkScript === "string" &&
350
+ /(?:^|&&|;)\s*bun\s+run\s+type-check(?:\s|$)/.test(checkScript)
351
+ );
352
+ }
353
+
276
354
  async function safeRunChecks(providerRoot: string): Promise<CheckResult[]> {
277
355
  try {
278
356
  return await runChecks(providerRoot);
@@ -322,12 +400,170 @@ function scoreBaseChecks(results: CheckResult[]): SubmitCheck[] {
322
400
  ];
323
401
  }
324
402
 
403
+ function scoreLocaleCatalog(
404
+ providerRoot: string,
405
+ provider: ProviderDefinition,
406
+ ): SubmitCheck {
407
+ const requiredKeys = collectProviderRequiredLocaleKeys(provider);
408
+ if (requiredKeys.length === 0) {
409
+ return pass(
410
+ "locale-catalog",
411
+ "operations",
412
+ "No key-owned provider metadata or operation metadata requires locale catalog validation.",
413
+ 0,
414
+ );
415
+ }
416
+
417
+ try {
418
+ const availableLocales = REQUIRED_PUBLIC_PROVIDER_LOCALES.filter((locale) =>
419
+ existsSync(join(providerRoot, "locales", `${locale}.json`)),
420
+ );
421
+ const catalogs = loadProviderLocaleCatalogs({
422
+ providerDir: providerRoot,
423
+ locales: availableLocales,
424
+ });
425
+ const validation = validateProviderLocaleCatalogs({
426
+ catalogs,
427
+ requiredLocales: REQUIRED_PUBLIC_PROVIDER_LOCALES,
428
+ requiredKeys,
429
+ });
430
+ if (!validation.ok) {
431
+ return blocker(
432
+ "locale-catalog",
433
+ "operations",
434
+ "Provider locale catalog is missing required public-provider copy.",
435
+ "Add provider-local locales/en.json and locales/ko.json values for every provider metadata key, operation descriptionKey, and .describeKey() or describeKey() schema field.",
436
+ 0,
437
+ validation.issues.map(
438
+ (issue) => `${issue.locale}:${issue.key}: ${issue.message}`,
439
+ ),
440
+ );
441
+ }
442
+ } catch (error) {
443
+ const message = error instanceof Error ? error.message : String(error);
444
+ return blocker(
445
+ "locale-catalog",
446
+ "operations",
447
+ "Provider locale catalog is missing required public-provider copy.",
448
+ "Add provider-local locales/en.json and locales/ko.json values for every provider metadata key, operation descriptionKey, and .describeKey() or describeKey() schema field.",
449
+ 0,
450
+ [`*:*: ${message}`],
451
+ );
452
+ }
453
+
454
+ return pass(
455
+ "locale-catalog",
456
+ "operations",
457
+ "Required provider and operation locale keys resolve in locales/en.json and locales/ko.json.",
458
+ 0,
459
+ );
460
+ }
461
+
462
+ function collectProviderRequiredLocaleKeys(
463
+ provider: ProviderDefinition,
464
+ ): string[] {
465
+ const keys = new Set<string>();
466
+
467
+ addLocaleKeys(keys, [
468
+ provider.meta.descriptionKey,
469
+ provider.meta.docTitleKey,
470
+ provider.meta.docDescriptionKey,
471
+ provider.meta.docSummaryKey,
472
+ provider.meta.docMarkdownKey,
473
+ ]);
474
+
475
+ const publicProfile = provider.meta.publicProfile;
476
+ if (publicProfile) {
477
+ addLocaleKeys(keys, [
478
+ publicProfile.displayNameKey,
479
+ publicProfile.shortDescriptionKey,
480
+ publicProfile.longDescriptionKey,
481
+ publicProfile.setupSummaryKey,
482
+ ...(publicProfile.capabilityKeys ?? []),
483
+ ...(publicProfile.examplePromptKeys ?? []),
484
+ ...(publicProfile.requirementKeys ?? []),
485
+ ...(publicProfile.limitationKeys ?? []),
486
+ ]);
487
+ }
488
+
489
+ for (const operation of Object.values(provider.operations)) {
490
+ addLocaleKeys(keys, [
491
+ operation.descriptionKey,
492
+ operation.docs?.titleKey,
493
+ operation.docs?.descriptionKey,
494
+ operation.docs?.summaryKey,
495
+ operation.docs?.markdownKey,
496
+ ...(operation.whenToUseKeys ?? []),
497
+ ...(operation.whenNotToUseKeys ?? []),
498
+ ...collectSchemaDescriptionKeys(operation.input),
499
+ ...collectSchemaDescriptionKeys(operation.output),
500
+ ]);
501
+ }
502
+
503
+ return Array.from(keys);
504
+ }
505
+
506
+ function addLocaleKeys(keys: Set<string>, values: readonly unknown[]): void {
507
+ for (const key of values) {
508
+ if (typeof key === "string" && key.length > 0) {
509
+ keys.add(key);
510
+ }
511
+ }
512
+ }
513
+
514
+ function collectSchemaDescriptionKeys(schema: unknown): string[] {
515
+ if (!(schema instanceof z.ZodType)) {
516
+ return [];
517
+ }
518
+ const jsonSchema = z.toJSONSchema(schema);
519
+ if (!isRecord(jsonSchema)) {
520
+ return [];
521
+ }
522
+ const keys: string[] = [];
523
+ collectJsonSchemaDescriptionKeys(jsonSchema, keys);
524
+ return keys;
525
+ }
526
+
527
+ function collectJsonSchemaDescriptionKeys(
528
+ schema: Record<string, unknown>,
529
+ keys: string[],
530
+ ): void {
531
+ const descriptionKey = schema[APIFUSE_DESCRIPTION_KEY_META_KEY];
532
+ if (typeof descriptionKey === "string" && descriptionKey.length > 0) {
533
+ keys.push(descriptionKey);
534
+ }
535
+
536
+ for (const value of Object.values(schema)) {
537
+ if (isRecord(value)) {
538
+ collectJsonSchemaDescriptionKeys(value, keys);
539
+ } else if (Array.isArray(value)) {
540
+ for (const item of value) {
541
+ if (isRecord(item)) {
542
+ collectJsonSchemaDescriptionKeys(item, keys);
543
+ }
544
+ }
545
+ }
546
+ }
547
+ }
548
+
549
+ function isRecord(value: unknown): value is Record<string, unknown> {
550
+ return typeof value === "object" && value !== null && !Array.isArray(value);
551
+ }
552
+
325
553
  function scoreOperationMetadata(provider: ProviderDefinition): SubmitCheck {
326
554
  const operations = Object.entries(provider.operations);
327
555
  const weakDescriptions = operations
328
- .filter(
329
- ([, operation]) => (operation.description ?? "").trim().length < 150,
330
- )
556
+ .filter(([, operation]) => {
557
+ // Hard-cut providers move operation copy into locale catalogs via
558
+ // descriptionKey instead of raw inline prose; the resolved text length
559
+ // is enforced at registry catalog-build time, matching how lintOperation
560
+ // skips the raw-description min-length rule when a descriptionKey is set.
561
+ const hasDescriptionKey =
562
+ typeof operation.descriptionKey === "string" &&
563
+ operation.descriptionKey.length > 0;
564
+ if (hasDescriptionKey) return false;
565
+ return true;
566
+ })
331
567
  .map(([operationId]) => operationId);
332
568
  const missingAnnotations = operations
333
569
  .filter(([, operation]) => !operation.annotations)
@@ -520,7 +756,7 @@ function scoreSmokeEvidence(smokeNote: string | undefined): SubmitCheck {
520
756
  maxPoints: CATEGORY_MAX_POINTS.smoke,
521
757
  message: "No local smoke evidence was provided.",
522
758
  remediation:
523
- "Start `bun run dev`, call `/health` and at least one `POST /v1/{operation}`, then rerun with `--smoke-note` or paste notes in the bounty issue.",
759
+ "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.",
524
760
  };
525
761
  }
526
762
 
@@ -784,7 +1020,7 @@ function blocker(
784
1020
 
785
1021
  export function renderText(report: SubmitCheckReport): string {
786
1022
  const lines = [
787
- `ApiFuse Provider Submission Score: ${report.score.total} / ${report.score.max}`,
1023
+ `APIFuse Provider Submission Score: ${report.score.total} / ${report.score.max}`,
788
1024
  `Verdict: ${report.score.verdict.toUpperCase()}`,
789
1025
  `Provider: ${report.provider.id}@${report.provider.version} (${report.provider.runtime}, auth: ${report.provider.authMode})`,
790
1026
  `Blockers: ${report.summary.blockers} Warnings: ${report.summary.warnings} Passed: ${report.summary.passed}`,
@@ -811,7 +1047,7 @@ export function renderText(report: SubmitCheckReport): string {
811
1047
 
812
1048
  export function renderMarkdown(report: SubmitCheckReport): string {
813
1049
  const lines = [
814
- "# ApiFuse Provider Submission Report",
1050
+ "# APIFuse Provider Submission Report",
815
1051
  "",
816
1052
  `- **Provider**: ${report.provider.id}@${report.provider.version}`,
817
1053
  `- **SDK**: ${report.provider.sdkVersion}`,
package/bin/apifuse.ts CHANGED
@@ -28,7 +28,7 @@ await module.main();
28
28
 
29
29
  function printHelp() {
30
30
  console.log(`
31
- apifuse - ApiFuse Provider SDK CLI
31
+ apifuse - APIFuse Provider SDK CLI
32
32
 
33
33
  Commands:`);
34
34
  for (const name of COMMAND_ORDER) {