@apifuse/provider-sdk 2.2.0-beta.40 → 2.2.0-beta.41

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 (44) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/bin/apifuse-check.ts +61 -0
  3. package/bin/apifuse-migrate-shape.ts +84 -0
  4. package/bin/apifuse-submit-check.ts +1760 -222
  5. package/dist/cli/commands.d.ts +1 -1
  6. package/dist/cli/commands.js +8 -0
  7. package/dist/cli/create.js +6 -0
  8. package/dist/cli/migrate-provider-shape.d.ts +52 -0
  9. package/dist/cli/migrate-provider-shape.js +515 -0
  10. package/dist/cli/templates/provider/provider.json.tpl +6 -0
  11. package/dist/contract.js +1 -0
  12. package/dist/define.js +22 -1
  13. package/dist/error-observability.d.ts +7 -0
  14. package/dist/error-observability.js +61 -0
  15. package/dist/errors.d.ts +15 -0
  16. package/dist/fixture-sanitization.js +13 -3
  17. package/dist/index.d.ts +1 -1
  18. package/dist/provider.d.ts +1 -1
  19. package/dist/runtime/executor.js +11 -1
  20. package/dist/server/error-observability.d.ts +1 -0
  21. package/dist/server/error-observability.js +1 -0
  22. package/dist/server/index.d.ts +2 -1
  23. package/dist/server/self-test.js +3 -0
  24. package/dist/server/serve-implementation.d.ts +12 -0
  25. package/dist/server/serve-implementation.js +135 -66
  26. package/dist/types.d.ts +18 -10
  27. package/package.json +3 -3
  28. package/src/cli/commands.ts +10 -0
  29. package/src/cli/create.ts +6 -0
  30. package/src/cli/migrate-provider-shape.ts +701 -0
  31. package/src/cli/templates/provider/provider.json.tpl +6 -0
  32. package/src/contract.ts +1 -0
  33. package/src/define.ts +33 -1
  34. package/src/error-observability.ts +64 -0
  35. package/src/errors.ts +16 -0
  36. package/src/fixture-sanitization.ts +19 -3
  37. package/src/index.ts +1 -0
  38. package/src/provider.ts +1 -0
  39. package/src/runtime/executor.ts +13 -1
  40. package/src/server/error-observability.ts +1 -0
  41. package/src/server/index.ts +2 -0
  42. package/src/server/self-test.ts +5 -0
  43. package/src/server/serve-implementation.ts +172 -84
  44. package/src/types.ts +38 -27
@@ -1,4 +1,4 @@
1
- export type ApifuseCommandName = "create" | "dev" | "check" | "sync-assets" | "submit-check" | "bounty-check" | "record" | "test" | "perf";
1
+ export type ApifuseCommandName = "create" | "dev" | "check" | "sync-assets" | "migrate-shape" | "submit-check" | "bounty-check" | "record" | "test" | "perf";
2
2
  export type ApifuseCommandManifest = {
3
3
  name: ApifuseCommandName;
4
4
  summary: string;
@@ -30,6 +30,13 @@ export const COMMAND_MANIFEST = {
30
30
  examples: ["apifuse sync-assets .", "apifuse sync-assets . --check"],
31
31
  modulePath: "./apifuse-sync-assets",
32
32
  },
33
+ "migrate-shape": {
34
+ name: "migrate-shape",
35
+ summary: "Migrate a provider index.ts from the single-phase defineProvider shape to the two-phase declaration builder.",
36
+ usage: "apifuse migrate-shape [path] [--check] [--json]",
37
+ examples: ["apifuse migrate-shape .", "apifuse migrate-shape . --check"],
38
+ modulePath: "./apifuse-migrate-shape",
39
+ },
33
40
  "submit-check": {
34
41
  name: "submit-check",
35
42
  summary: "Score provider bounty submission readiness and emit checklist evidence.",
@@ -81,6 +88,7 @@ export const COMMAND_ORDER = [
81
88
  "dev",
82
89
  "check",
83
90
  "sync-assets",
91
+ "migrate-shape",
84
92
  "submit-check",
85
93
  "record",
86
94
  "test",
@@ -379,6 +379,12 @@ export async function buildProviderCreatePlan(options, cwd) {
379
379
  sdkSpecifier,
380
380
  }),
381
381
  },
382
+ {
383
+ path: resolve(providerRoot, "provider.json"),
384
+ content: await renderTemplate("provider.json.tpl", {
385
+ PROVIDER_ID: options.name,
386
+ }),
387
+ },
382
388
  {
383
389
  path: resolve(providerRoot, "Dockerfile"),
384
390
  content: await renderTemplate("Dockerfile.tpl", {}),
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Provider authoring shape migration.
3
+ *
4
+ * `defineProvider` changed in 2.2.0-beta.37 from returning a finished provider
5
+ * to returning a builder, splitting authoring into two phases:
6
+ *
7
+ * const buildProvider = defineProvider(<declaration>);
8
+ * export type ProviderContext = ProviderContextOf<typeof buildProvider>;
9
+ * export default buildProvider({ operations });
10
+ *
11
+ * Source written against the single-phase shape still type-checks against an
12
+ * older pin but default-exports a builder function once the pin moves, so
13
+ * `loadProviderDefinition` rejects it. Bumping the pin without migrating the
14
+ * source therefore breaks the module. This transform performs the source half
15
+ * so the SDK bump fan-out can ship both in one commit.
16
+ *
17
+ * The transform is deliberately conservative: it rewrites only the shapes it
18
+ * can fully account for and reports `skipped` with a reason for anything else,
19
+ * rather than emitting a partial migration a reviewer would have to audit.
20
+ */
21
+ /** Every source shape this transform recognizes. */
22
+ export type ProviderShapeKind =
23
+ /** `export default defineProvider({ ..., operations })` */
24
+ "single-phase-default-export"
25
+ /** `const p = defineProvider({ ..., operations }); export default p;` */
26
+ | "single-phase-variable-export"
27
+ /** `const p = defineProvider({ ..., operations }); export default { ...p, deployment };` */
28
+ | "single-phase-variable-spread-export"
29
+ /** Already `const b = defineProvider(...); export default b({ operations })` */
30
+ | "two-phase";
31
+ export type ProviderShapeMigration = {
32
+ readonly status: "migrated";
33
+ readonly kind: ProviderShapeKind;
34
+ readonly code: string;
35
+ /** Source text the operations map was supplied as, for reporting. */
36
+ readonly operationsExpression: string;
37
+ } | {
38
+ readonly status: "unchanged";
39
+ readonly kind: "two-phase";
40
+ readonly code: string;
41
+ } | {
42
+ readonly status: "skipped";
43
+ readonly reason: string;
44
+ };
45
+ /**
46
+ * Migrate one provider `index.ts` to the two-phase authoring shape.
47
+ *
48
+ * Returns the rewritten source on success. Callers MUST treat `skipped` as a
49
+ * hard stop for that provider — a skipped provider needs a human, and pairing
50
+ * a pin bump with a skipped migration produces an unloadable module.
51
+ */
52
+ export declare function migrateProviderShape(sourceText: string, fileName?: string): ProviderShapeMigration;
@@ -0,0 +1,515 @@
1
+ import ts from "typescript";
2
+ const DECLARATION_BUILDER_NAME = "buildProvider";
3
+ const PROVIDER_CONTEXT_TYPE_NAME = "ProviderContext";
4
+ const PROVIDER_CONTEXT_OF_TYPE_NAME = "ProviderContextOf";
5
+ const PROVIDER_SDK_PROVIDER_SUBPATH = "@apifuse/provider-sdk/provider";
6
+ /**
7
+ * Migrate one provider `index.ts` to the two-phase authoring shape.
8
+ *
9
+ * Returns the rewritten source on success. Callers MUST treat `skipped` as a
10
+ * hard stop for that provider — a skipped provider needs a human, and pairing
11
+ * a pin bump with a skipped migration produces an unloadable module.
12
+ */
13
+ export function migrateProviderShape(sourceText, fileName = "index.ts") {
14
+ const source = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest,
15
+ /* setParentNodes */ true, ts.ScriptKind.TS);
16
+ const syntaxError = firstSyntaxError(source);
17
+ if (syntaxError !== undefined) {
18
+ return { status: "skipped", reason: syntaxError };
19
+ }
20
+ const calls = collectDefineProviderCalls(source);
21
+ if (calls.length === 0) {
22
+ return {
23
+ status: "skipped",
24
+ reason: "No defineProvider(...) call found; this file does not declare a provider.",
25
+ };
26
+ }
27
+ if (calls.length > 1) {
28
+ return {
29
+ status: "skipped",
30
+ reason: `Found ${calls.length} defineProvider(...) calls; the transform rewrites exactly one declaration.`,
31
+ };
32
+ }
33
+ const call = calls[0];
34
+ if (call === undefined) {
35
+ return { status: "skipped", reason: "Internal: defineProvider call vanished." };
36
+ }
37
+ const exportAssignment = findDefaultExport(source);
38
+ if (exportAssignment === undefined) {
39
+ return {
40
+ status: "skipped",
41
+ reason: "No `export default` found; the provider module must default-export its provider.",
42
+ };
43
+ }
44
+ const declaration = call.arguments[0];
45
+ if (declaration === undefined || !ts.isObjectLiteralExpression(declaration)) {
46
+ return {
47
+ status: "skipped",
48
+ reason: "defineProvider(...) is called with a non-literal argument, so the declaration's operations key cannot be located.",
49
+ };
50
+ }
51
+ const operationsProperty = findOperationsProperty(declaration);
52
+ // Already migrated: the declaration carries no operations key and the
53
+ // default export path calls a builder variable rather than defineProvider.
54
+ if (operationsProperty === undefined &&
55
+ isAlreadyTwoPhase(source, exportAssignment, call)) {
56
+ return { status: "unchanged", kind: "two-phase", code: sourceText };
57
+ }
58
+ if (operationsProperty === undefined) {
59
+ return {
60
+ status: "skipped",
61
+ reason: "The declaration has no `operations` key and the default export does not call a declaration builder, so the intended shape is ambiguous.",
62
+ };
63
+ }
64
+ const operationsText = operationsPropertyValueText(operationsProperty, source);
65
+ if (operationsText === undefined) {
66
+ return {
67
+ status: "skipped",
68
+ reason: "The `operations` property uses a form the transform cannot relocate (getter, setter, method, spread, or computed name).",
69
+ };
70
+ }
71
+ const shape = classifyShape(source, call, exportAssignment);
72
+ if (shape.status === "skipped") {
73
+ return shape;
74
+ }
75
+ const edits = [];
76
+ const builderName = pickBuilderName(source);
77
+ // 1. Remove `operations` from the declaration literal.
78
+ edits.push(...removeOperationsProperty(operationsProperty, declaration, source));
79
+ // 2. Bind the declaration to `const buildProvider = defineProvider({...})`.
80
+ edits.push(...introduceBuilder(source, call, shape, builderName));
81
+ // 3. Route the default export through `buildProvider({ operations })`.
82
+ edits.push(...rewriteDefaultExport(source, exportAssignment, shape, builderName, operationsText));
83
+ // 4. `export type ProviderContext = ProviderContextOf<typeof buildProvider>;`
84
+ // plus the type-only import, when neither is already present.
85
+ edits.push(...ensureProviderContextType(source, call, shape, builderName));
86
+ const code = applyEdits(sourceText, edits);
87
+ // Re-parse the output: a transform that emits unparseable source is worse
88
+ // than one that skips, because the pin bump would ship alongside it.
89
+ const verified = ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
90
+ const outputError = firstSyntaxError(verified);
91
+ if (outputError !== undefined) {
92
+ return {
93
+ status: "skipped",
94
+ reason: `Transform produced source that does not parse (${outputError}); refusing to emit a partial migration.`,
95
+ };
96
+ }
97
+ return {
98
+ status: "migrated",
99
+ kind: shape.kind,
100
+ code,
101
+ operationsExpression: operationsText,
102
+ };
103
+ }
104
+ function classifyShape(source, call, exportAssignment) {
105
+ const variableStatement = enclosingVariableStatement(call);
106
+ if (variableStatement === undefined) {
107
+ // `export default defineProvider({...})`
108
+ if (exportAssignment.expression === call) {
109
+ return { status: "ok", kind: "single-phase-default-export" };
110
+ }
111
+ return {
112
+ status: "skipped",
113
+ reason: "defineProvider(...) is neither bound to a variable nor the default-export expression, so the transform cannot place the builder.",
114
+ };
115
+ }
116
+ const declarations = variableStatement.declarationList.declarations;
117
+ if (declarations.length !== 1) {
118
+ return {
119
+ status: "skipped",
120
+ reason: "The defineProvider(...) result is declared alongside other bindings in one statement; split the declaration first.",
121
+ };
122
+ }
123
+ const declaration = declarations[0];
124
+ if (declaration === undefined || !ts.isIdentifier(declaration.name)) {
125
+ return {
126
+ status: "skipped",
127
+ reason: "The defineProvider(...) result is bound to a destructuring pattern.",
128
+ };
129
+ }
130
+ if (declaration.initializer !== call) {
131
+ return {
132
+ status: "skipped",
133
+ reason: "defineProvider(...) is nested inside a larger initializer expression the transform cannot rewrite.",
134
+ };
135
+ }
136
+ const variableName = declaration.name.text;
137
+ const exported = exportAssignment.expression;
138
+ if (ts.isIdentifier(exported) && exported.text === variableName) {
139
+ return {
140
+ status: "ok",
141
+ kind: "single-phase-variable-export",
142
+ variableName,
143
+ variableStatement,
144
+ };
145
+ }
146
+ if (ts.isObjectLiteralExpression(exported)) {
147
+ const spreads = exported.properties.filter(ts.isSpreadAssignment);
148
+ const spreadsProvider = spreads.some((property) => ts.isIdentifier(property.expression) &&
149
+ property.expression.text === variableName);
150
+ if (!spreadsProvider) {
151
+ return {
152
+ status: "skipped",
153
+ reason: `The default export is an object literal that does not spread \`${variableName}\`, so the provider value it exports is unclear.`,
154
+ };
155
+ }
156
+ if (spreads.length > 1) {
157
+ return {
158
+ status: "skipped",
159
+ reason: "The default export spreads more than one value; the transform cannot tell which carries the provider.",
160
+ };
161
+ }
162
+ const extras = exported.properties.filter((property) => !ts.isSpreadAssignment(property));
163
+ return {
164
+ status: "ok",
165
+ kind: "single-phase-variable-spread-export",
166
+ variableName,
167
+ variableStatement,
168
+ spreadExportExtras: extras
169
+ .map((property) => property.getText(source))
170
+ .join(",\n "),
171
+ };
172
+ }
173
+ return {
174
+ status: "skipped",
175
+ reason: `The default export is neither \`${variableName}\` nor an object literal spreading it.`,
176
+ };
177
+ }
178
+ function collectDefineProviderCalls(source) {
179
+ const calls = [];
180
+ const visit = (node) => {
181
+ if (ts.isCallExpression(node) &&
182
+ ts.isIdentifier(node.expression) &&
183
+ node.expression.text === "defineProvider") {
184
+ calls.push(node);
185
+ }
186
+ ts.forEachChild(node, visit);
187
+ };
188
+ visit(source);
189
+ return calls;
190
+ }
191
+ function findDefaultExport(source) {
192
+ for (const statement of source.statements) {
193
+ if (ts.isExportAssignment(statement) && statement.isExportEquals !== true) {
194
+ return statement;
195
+ }
196
+ }
197
+ return undefined;
198
+ }
199
+ function defaultExportCallsBuilder(exportAssignment, declarationCall) {
200
+ const expression = exportAssignment.expression;
201
+ if (!ts.isCallExpression(expression))
202
+ return false;
203
+ if (expression === declarationCall)
204
+ return false;
205
+ return ts.isIdentifier(expression.expression);
206
+ }
207
+ /**
208
+ * True when the module is already in the two-phase shape: the default export
209
+ * resolves to a builder-call result. Covers the three migrated layouts —
210
+ * `export default buildProvider({...})`, an intermediate
211
+ * `const provider = buildProvider({...}); export default provider;`, and the
212
+ * spread export `export default { ...provider, deployment }` over such a
213
+ * binding. Without the latter two, re-running the transform on its own
214
+ * spread-shape output reports "ambiguous" instead of "unchanged", which
215
+ * breaks idempotency for repeated fan-out runs.
216
+ */
217
+ function isAlreadyTwoPhase(source, exportAssignment, declarationCall) {
218
+ if (defaultExportCallsBuilder(exportAssignment, declarationCall))
219
+ return true;
220
+ const exported = exportAssignment.expression;
221
+ const candidateNames = [];
222
+ if (ts.isIdentifier(exported)) {
223
+ candidateNames.push(exported.text);
224
+ }
225
+ else if (ts.isObjectLiteralExpression(exported)) {
226
+ for (const property of exported.properties) {
227
+ if (ts.isSpreadAssignment(property) && ts.isIdentifier(property.expression)) {
228
+ candidateNames.push(property.expression.text);
229
+ }
230
+ }
231
+ }
232
+ if (candidateNames.length === 0)
233
+ return false;
234
+ // Does any spread/exported identifier bind a call to an identifier other
235
+ // than defineProvider — i.e. a builder call?
236
+ let found = false;
237
+ const visit = (node) => {
238
+ if (found)
239
+ return;
240
+ if (ts.isVariableDeclaration(node) &&
241
+ ts.isIdentifier(node.name) &&
242
+ candidateNames.includes(node.name.text) &&
243
+ node.initializer !== undefined &&
244
+ ts.isCallExpression(node.initializer) &&
245
+ node.initializer !== declarationCall &&
246
+ ts.isIdentifier(node.initializer.expression) &&
247
+ node.initializer.expression.text !== "defineProvider") {
248
+ found = true;
249
+ return;
250
+ }
251
+ ts.forEachChild(node, visit);
252
+ };
253
+ visit(source);
254
+ return found;
255
+ }
256
+ function findOperationsProperty(declaration) {
257
+ for (const property of declaration.properties) {
258
+ if (ts.isSpreadAssignment(property))
259
+ continue;
260
+ const name = property.name;
261
+ if (name === undefined)
262
+ continue;
263
+ if ((ts.isIdentifier(name) || ts.isStringLiteral(name)) &&
264
+ name.text === "operations") {
265
+ return property;
266
+ }
267
+ }
268
+ return undefined;
269
+ }
270
+ /**
271
+ * Source text of the value to hand the builder. Shorthand becomes the bare
272
+ * identifier so `{ operations }` stays idiomatic; a property assignment keeps
273
+ * its initializer verbatim, including a multi-line inline map.
274
+ */
275
+ function operationsPropertyValueText(property, source) {
276
+ if (ts.isShorthandPropertyAssignment(property)) {
277
+ return property.name.text;
278
+ }
279
+ if (ts.isPropertyAssignment(property)) {
280
+ return property.initializer.getText(source);
281
+ }
282
+ return undefined;
283
+ }
284
+ function removeOperationsProperty(property, declaration, source) {
285
+ const properties = declaration.properties;
286
+ const index = properties.indexOf(property);
287
+ const start = property.getFullStart();
288
+ let end = property.getEnd();
289
+ // Absorb the trailing comma so the remaining literal stays well-formed,
290
+ // whether the key sat mid-list or last.
291
+ const text = source.getFullText();
292
+ let cursor = end;
293
+ while (cursor < text.length && /\s/.test(text.charAt(cursor)))
294
+ cursor += 1;
295
+ if (text.charAt(cursor) === ",") {
296
+ end = cursor + 1;
297
+ }
298
+ else if (index > 0) {
299
+ // Last property with no trailing comma: drop the preceding one instead.
300
+ const previous = properties[index - 1];
301
+ if (previous !== undefined) {
302
+ let back = previous.getEnd();
303
+ while (back < text.length && /\s/.test(text.charAt(back)))
304
+ back += 1;
305
+ if (text.charAt(back) === ",") {
306
+ return [{ start: back, end, text: "" }];
307
+ }
308
+ }
309
+ }
310
+ return [{ start, end, text: "" }];
311
+ }
312
+ function introduceBuilder(source, call, shape, builderName) {
313
+ if (shape.kind === "single-phase-default-export") {
314
+ // `export default defineProvider({...})` becomes a standalone builder
315
+ // declaration; the default export is re-appended separately.
316
+ const exportStatement = call.parent;
317
+ if (!ts.isExportAssignment(exportStatement))
318
+ return [];
319
+ return [
320
+ {
321
+ start: exportStatement.getStart(source),
322
+ end: call.getStart(source),
323
+ text: `const ${builderName} = `,
324
+ },
325
+ ];
326
+ }
327
+ const statement = shape.variableStatement;
328
+ if (statement === undefined || shape.variableName === undefined)
329
+ return [];
330
+ // Rename the existing binding to the builder name. The old name is
331
+ // re-introduced for the built provider only in the spread-export shape,
332
+ // which needs an intermediate value to spread.
333
+ const declaration = statement.declarationList.declarations[0];
334
+ if (declaration === undefined || !ts.isIdentifier(declaration.name))
335
+ return [];
336
+ return [
337
+ {
338
+ start: declaration.name.getStart(source),
339
+ end: declaration.name.getEnd(),
340
+ text: builderName,
341
+ },
342
+ ];
343
+ }
344
+ function rewriteDefaultExport(source, exportAssignment, shape, builderName, operationsText) {
345
+ const operationsArgument = operationsText === "operations"
346
+ ? "{ operations }"
347
+ : `{ operations: ${operationsText} }`;
348
+ const call = `${builderName}(${operationsArgument})`;
349
+ if (shape.kind === "single-phase-default-export") {
350
+ // The builder declaration replaced the `export default` prefix; the
351
+ // export itself is appended after what is now the builder statement.
352
+ return [
353
+ {
354
+ start: exportAssignment.getEnd(),
355
+ end: exportAssignment.getEnd(),
356
+ text: `\n\nexport default ${call};`,
357
+ },
358
+ ];
359
+ }
360
+ if (shape.kind === "single-phase-variable-export") {
361
+ return [
362
+ {
363
+ start: exportAssignment.expression.getStart(source),
364
+ end: exportAssignment.expression.getEnd(),
365
+ text: call,
366
+ },
367
+ ];
368
+ }
369
+ // Spread export: re-introduce the provider binding so the extra properties
370
+ // (currently `deployment`) still spread over a built provider.
371
+ const providerName = shape.variableName ?? "provider";
372
+ const extras = shape.spreadExportExtras ?? "";
373
+ const rebuilt = extras.length > 0
374
+ ? `{\n ...${providerName},\n ${extras},\n}`
375
+ : `{ ...${providerName} }`;
376
+ return [
377
+ {
378
+ start: exportAssignment.getStart(source),
379
+ end: exportAssignment.getStart(source),
380
+ text: `const ${providerName} = ${call};\n\n`,
381
+ },
382
+ {
383
+ start: exportAssignment.expression.getStart(source),
384
+ end: exportAssignment.expression.getEnd(),
385
+ text: rebuilt,
386
+ },
387
+ ];
388
+ }
389
+ /**
390
+ * Add `export type ProviderContext = ProviderContextOf<typeof buildProvider>`
391
+ * and the type-only import when absent. Operation handlers reference this
392
+ * type, so a migration that omits it leaves the provider without the context
393
+ * type the two-phase shape exists to provide.
394
+ */
395
+ function ensureProviderContextType(source, call, shape, builderName) {
396
+ const text = source.getFullText();
397
+ const edits = [];
398
+ const hasContextTypeAlias = source.statements.some((statement) => ts.isTypeAliasDeclaration(statement) &&
399
+ statement.name.text === PROVIDER_CONTEXT_TYPE_NAME);
400
+ if (!hasContextTypeAlias) {
401
+ // After the builder statement — which is the variable statement when the
402
+ // declaration was bound, or the rewritten export statement otherwise.
403
+ const anchor = shape.variableStatement ??
404
+ (ts.isExportAssignment(call.parent) ? call.parent : undefined);
405
+ if (anchor !== undefined) {
406
+ const insertAt = anchor.getEnd();
407
+ edits.push({
408
+ start: insertAt,
409
+ end: insertAt,
410
+ text: `\n\nexport type ${PROVIDER_CONTEXT_TYPE_NAME} = ${PROVIDER_CONTEXT_OF_TYPE_NAME}<typeof ${builderName}>;`,
411
+ });
412
+ }
413
+ }
414
+ if (text.includes(PROVIDER_CONTEXT_OF_TYPE_NAME)) {
415
+ return edits;
416
+ }
417
+ const providerImport = findProviderSdkImport(source);
418
+ if (providerImport === undefined) {
419
+ // No named import from the provider subpath to extend; adding a new
420
+ // import line without knowing the module's style is riskier than
421
+ // leaving the type import to `bun run check` feedback.
422
+ return edits;
423
+ }
424
+ const named = providerImport.importClause?.namedBindings;
425
+ if (named === undefined || !ts.isNamedImports(named)) {
426
+ return edits;
427
+ }
428
+ const last = named.elements[named.elements.length - 1];
429
+ if (last === undefined) {
430
+ return edits;
431
+ }
432
+ edits.push({
433
+ start: last.getEnd(),
434
+ end: last.getEnd(),
435
+ text: `, type ${PROVIDER_CONTEXT_OF_TYPE_NAME}`,
436
+ });
437
+ return edits;
438
+ }
439
+ function findProviderSdkImport(source) {
440
+ for (const statement of source.statements) {
441
+ if (!ts.isImportDeclaration(statement))
442
+ continue;
443
+ const moduleSpecifier = statement.moduleSpecifier;
444
+ if (!ts.isStringLiteral(moduleSpecifier))
445
+ continue;
446
+ if (moduleSpecifier.text !== PROVIDER_SDK_PROVIDER_SUBPATH)
447
+ continue;
448
+ const named = statement.importClause?.namedBindings;
449
+ if (named !== undefined && ts.isNamedImports(named)) {
450
+ const importsDefineProvider = named.elements.some((element) => element.name.text === "defineProvider");
451
+ if (importsDefineProvider)
452
+ return statement;
453
+ }
454
+ }
455
+ return undefined;
456
+ }
457
+ /**
458
+ * `buildProvider` unless the module already binds that name, in which case a
459
+ * numbered suffix keeps the transform from shadowing an existing binding.
460
+ */
461
+ function pickBuilderName(source) {
462
+ const taken = new Set();
463
+ const visit = (node) => {
464
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
465
+ taken.add(node.name.text);
466
+ }
467
+ if (ts.isFunctionDeclaration(node) && node.name !== undefined) {
468
+ taken.add(node.name.text);
469
+ }
470
+ if (ts.isImportSpecifier(node)) {
471
+ taken.add(node.name.text);
472
+ }
473
+ ts.forEachChild(node, visit);
474
+ };
475
+ visit(source);
476
+ if (!taken.has(DECLARATION_BUILDER_NAME))
477
+ return DECLARATION_BUILDER_NAME;
478
+ for (let suffix = 2; suffix < 100; suffix += 1) {
479
+ const candidate = `${DECLARATION_BUILDER_NAME}${suffix}`;
480
+ if (!taken.has(candidate))
481
+ return candidate;
482
+ }
483
+ return `${DECLARATION_BUILDER_NAME}Migrated`;
484
+ }
485
+ function enclosingVariableStatement(node) {
486
+ let current = node.parent;
487
+ while (current !== undefined) {
488
+ if (ts.isVariableStatement(current))
489
+ return current;
490
+ if (ts.isSourceFile(current))
491
+ return undefined;
492
+ current = current.parent;
493
+ }
494
+ return undefined;
495
+ }
496
+ function firstSyntaxError(source) {
497
+ const diagnostics = source.parseDiagnostics;
498
+ if (diagnostics === undefined || diagnostics.length === 0)
499
+ return undefined;
500
+ const first = diagnostics[0];
501
+ if (first === undefined)
502
+ return undefined;
503
+ const message = ts.flattenDiagnosticMessageText(first.messageText, " ");
504
+ const { line } = source.getLineAndCharacterOfPosition(first.start);
505
+ return `${message} (line ${line + 1})`;
506
+ }
507
+ /** Apply edits back-to-front so earlier offsets stay valid. */
508
+ function applyEdits(text, edits) {
509
+ const ordered = [...edits].sort((a, b) => b.start - a.start || b.end - a.end);
510
+ let output = text;
511
+ for (const edit of ordered) {
512
+ output = output.slice(0, edit.start) + edit.text + output.slice(edit.end);
513
+ }
514
+ return output;
515
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "providerId": "{{PROVIDER_ID}}",
4
+ "owner": "bounty",
5
+ "lifecycle": "draft"
6
+ }
package/dist/contract.js CHANGED
@@ -130,6 +130,7 @@ function extractHealthCheck(value) {
130
130
  name: item.name,
131
131
  description: item.description,
132
132
  input: toJsonValue(item.input),
133
+ scenario: toJsonValue(item.scenario),
133
134
  degradedThresholdMs: item.degradedThresholdMs,
134
135
  timeoutMs: item.timeoutMs,
135
136
  expectedStatus: item.expectedStatus,
package/dist/define.js CHANGED
@@ -2,6 +2,7 @@ import ms from "ms";
2
2
  import { validateFailClosedOperationDeclaration, validateFailClosedProviderDeclaration, } from "./declaration-validation.js";
3
3
  import { SDK_RUNTIME_OWNED_ERROR_CODES } from "./error-resolution.js";
4
4
  import { ProviderError, ValidationError } from "./errors.js";
5
+ import { HealthScenarioSchema } from "./health-scenario.js";
5
6
  import { NativeEgressPolicyValidationError, validateNativeProviderConfig, } from "./native-egress-policy.js";
6
7
  import { safeParseSchemaSync } from "./schema.js";
7
8
  import { resolveHealthCheckInputDateTokens } from "./server/self-test-input-tokens.js";
@@ -1136,6 +1137,7 @@ const HEALTH_CHECK_CASE_FIELDS = new Set([
1136
1137
  "input",
1137
1138
  "prepareInput",
1138
1139
  "assertions",
1140
+ "scenario",
1139
1141
  "degradedThresholdMs",
1140
1142
  "timeoutMs",
1141
1143
  "expectedStatus",
@@ -1310,12 +1312,31 @@ function validateHealthCheckCase(providerId, operationName, caseValue, caseIndex
1310
1312
  const c = caseValue;
1311
1313
  if (typeof c.name !== "string" || c.name.length === 0)
1312
1314
  throw new ValidationError(`Provider "${providerId}" ${fieldPath}.name must be a non-empty string.`);
1313
- if (typeof c.assertions !== "function")
1315
+ const hasScenario = c.scenario !== undefined;
1316
+ const imperativeFields = [
1317
+ ...(c.prepareInput === undefined ? [] : ["prepareInput"]),
1318
+ ...(c.assertions === undefined ? [] : ["assertions"]),
1319
+ ];
1320
+ if (hasScenario && imperativeFields.length > 0)
1321
+ throw new ValidationError(`Provider "${providerId}" operation "${operationName}" health-check case "${c.name}" cannot declare scenario with ${imperativeFields.join(" and ")}.`, {
1322
+ fix: `Remove ${imperativeFields.map((field) => `${fieldPath}.${field}`).join(" and ")} and keep ${fieldPath}.scenario, or remove ${fieldPath}.scenario to keep the imperative hooks.`,
1323
+ });
1324
+ if (!hasScenario && typeof c.assertions !== "function")
1314
1325
  throw new ValidationError(`Provider "${providerId}" ${fieldPath}.assertions must be a function.`, {
1315
1326
  fix: `Set ${fieldPath}.assertions to (ctx) => { ... } that throws on failure.`,
1316
1327
  });
1317
1328
  if (c.prepareInput !== undefined && typeof c.prepareInput !== "function")
1318
1329
  throw new ValidationError(`Provider "${providerId}" ${fieldPath}.prepareInput must be a function.`);
1330
+ if (hasScenario) {
1331
+ const parsedScenario = HealthScenarioSchema.safeParse(c.scenario);
1332
+ if (!parsedScenario.success)
1333
+ throw new ValidationError(`Provider "${providerId}" operation "${operationName}" health-check case "${c.name}" has an invalid scenario: it must conform to HealthScenario.`, { fix: `Build ${fieldPath}.scenario with defineHealthScenario().` });
1334
+ const unrelatedOperation = parsedScenario.data.coversOperations.find((operationId) => operationId !== operationName);
1335
+ if (unrelatedOperation !== undefined)
1336
+ throw new ValidationError(`Provider "${providerId}" operation "${operationName}" health-check case "${c.name}" scenario.coversOperations cannot claim unrelated operation "${unrelatedOperation}".`, {
1337
+ fix: `Set ${fieldPath}.scenario.coversOperations to ["${operationName}"].`,
1338
+ });
1339
+ }
1319
1340
  if (c.degradedThresholdMs !== undefined &&
1320
1341
  (typeof c.degradedThresholdMs !== "number" ||
1321
1342
  !Number.isInteger(c.degradedThresholdMs) ||
@@ -0,0 +1,7 @@
1
+ import { type ProviderErrorObservability } from "./errors.js";
2
+ /**
3
+ * Extracts only own data properties from branded provider errors. In
4
+ * particular, descriptor reads reject options/observability accessors without
5
+ * invoking provider-controlled getters.
6
+ */
7
+ export declare function safeProviderErrorObservability(error: unknown): ProviderErrorObservability | undefined;