shapi-cli 0.1.0 → 0.2.0

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 (55) hide show
  1. package/README.md +2 -1
  2. package/advisories/anthropic-api-version-2026-10-21-sampling-params-400.json +35 -0
  3. package/advisories/google-ads-api-v22-sunset.json +27 -0
  4. package/advisories/google-custom-search-json-api-shutdown.json +27 -0
  5. package/advisories/google-gemini-2-0-flash-shutdown.json +27 -0
  6. package/advisories/google-gemini-2-5-flash-image-shutdown.json +27 -0
  7. package/advisories/google-gemini-2-5-preview-tts-shutdown.json +27 -0
  8. package/advisories/hubspot-contact-lists-v1-sunset.json +28 -0
  9. package/advisories/hubspot-contacts-v1-list-memberships-removed.json +30 -0
  10. package/advisories/hubspot-marketing-emails-folderid-removed.json +39 -0
  11. package/advisories/hubspot-marketing-emails-v1-sunset.json +24 -0
  12. package/advisories/hubspot-pipelines-v1-sunset.json +22 -2
  13. package/advisories/meta-graph-commerce-order-management-removed.json +26 -0
  14. package/advisories/meta-graph-date-format-param-removed.json +30 -0
  15. package/advisories/meta-graph-delivery-estimate-fields-removed.json +30 -0
  16. package/advisories/meta-graph-instagram-oembed-fields-removed.json +33 -0
  17. package/advisories/meta-graph-marketing-api-v24-sunset.json +34 -0
  18. package/advisories/meta-graph-marketing-api-v25-sunset.json +35 -0
  19. package/advisories/meta-graph-page-insights-impressions-fans-removed.json +51 -0
  20. package/advisories/meta-graph-page-insights-reach-video-views-removed.json +57 -0
  21. package/advisories/meta-graph-root-ids-lookup-removed.json +30 -0
  22. package/advisories/meta-graph-v20-sunset.json +10 -1
  23. package/advisories/meta-graph-v21-sunset.json +10 -1
  24. package/advisories/openai-assistants-api-shutdown.json +27 -0
  25. package/advisories/openai-audio-translations-endpoint-shutdown.json +27 -0
  26. package/advisories/openai-evals-api-shutdown.json +27 -0
  27. package/advisories/quickbooks-companyinfo-neoenabled-removed.json +29 -0
  28. package/advisories/quickbooks-minorversion-below-75-ignored.json +13 -1
  29. package/advisories/quickbooks-refresh-token-five-year-hard-expiry.json +29 -0
  30. package/advisories/salesforce-soap-login-broad-ip-range-token-required.json +30 -0
  31. package/advisories/salesforce-soap-login-disabled-when-unused.json +30 -0
  32. package/advisories/salesforce-soap-login-use-any-api-auth-required.json +31 -0
  33. package/advisories/shopify-admin-api-2026-01-unsupported.json +33 -0
  34. package/advisories/shopify-admin-api-2026-04-unsupported.json +33 -0
  35. package/advisories/shopify-global-catalog-rest-api-shutdown.json +24 -0
  36. package/advisories/shopify-non-expiring-offline-tokens-rejected.json +27 -0
  37. package/advisories/shopify-script-tags-stop-running.json +27 -0
  38. package/advisories/slack-files-upload-retired.json +28 -0
  39. package/advisories/stripe-checkout-session-ui-mode-values-renamed.json +53 -0
  40. package/advisories/twilio-conference-list-default-in-progress.json +27 -0
  41. package/advisories/twilio-notify-end-of-life.json +28 -0
  42. package/advisories/twilio-rcs-undelivered-error-code-30046.json +27 -0
  43. package/advisories/twilio-voice-insights-conference-participant-removed.json +28 -0
  44. package/advisories/x-activity-api-pascalcase-event-types-removed.json +31 -0
  45. package/advisories/x-self-serve-follow-writes-removed.json +27 -0
  46. package/advisories/x-self-serve-like-writes-removed.json +27 -0
  47. package/advisories/xero-allocations-contact-mismatch-rejected.json +27 -0
  48. package/advisories/xero-au-payroll-isqualifyingearnings-required.json +28 -0
  49. package/advisories/xero-finance-accounting-activities-decommissioned.json +28 -0
  50. package/advisories/xero-invoices-email-daily-limit-429.json +32 -0
  51. package/advisories/xero-nz-payroll-employee-tax-kiwisaverstatus-required.json +27 -0
  52. package/advisories/xero-organisation-class-ultra-enum.json +31 -0
  53. package/dist/index.js +1333 -153
  54. package/dist/tree-sitter-python.wasm +0 -0
  55. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { Command as Command2, CommanderError as CommanderError2, Option } from "
3
3
 
4
4
  // packages/cli/src/fix.ts
5
5
  import { execFileSync as execFileSync3 } from "child_process";
6
- import { existsSync as existsSync4, mkdirSync as mkdirSync5, mkdtempSync as mkdtempSync3, readdirSync as readdirSync3, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
6
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, mkdtempSync as mkdtempSync3, readdirSync as readdirSync3, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
7
7
  import os3 from "os";
8
8
  import path12 from "path";
9
9
 
@@ -51,10 +51,34 @@ var providers_default = [
51
51
  "subscriptions.update": "POST /v1/subscriptions/{id}",
52
52
  "invoices.create": "POST /v1/invoices",
53
53
  "checkout.sessions.create": "POST /v1/checkout/sessions",
54
+ "checkout.sessions.retrieve": "GET /v1/checkout/sessions/{session}",
54
55
  "refunds.create": "POST /v1/refunds"
55
56
  },
56
57
  docsUrl: "https://docs.stripe.com/api",
57
- changelogUrl: "https://docs.stripe.com/changelog"
58
+ changelogUrl: "https://docs.stripe.com/changelog",
59
+ python: {
60
+ packages: [
61
+ { pypi: "stripe", module: "stripe" }
62
+ ],
63
+ methods: {
64
+ "checkout.Session.create": "POST /v1/checkout/sessions",
65
+ "checkout.Session.retrieve": "GET /v1/checkout/sessions/{session}",
66
+ "PaymentIntent.create": "POST /v1/payment_intents",
67
+ "PaymentIntent.retrieve": "GET /v1/payment_intents/{intent}",
68
+ "PaymentIntent.confirm": "POST /v1/payment_intents/{intent}/confirm",
69
+ "SetupIntent.create": "POST /v1/setup_intents",
70
+ "Customer.create": "POST /v1/customers",
71
+ "Customer.list": "GET /v1/customers",
72
+ "Subscription.create": "POST /v1/subscriptions",
73
+ "Balance.retrieve": "GET /v1/balance",
74
+ "checkout.sessions.create": "POST /v1/checkout/sessions",
75
+ "checkout.sessions.retrieve": "GET /v1/checkout/sessions/{session}",
76
+ "payment_intents.create": "POST /v1/payment_intents",
77
+ "payment_intents.retrieve": "GET /v1/payment_intents/{intent}",
78
+ "customers.create": "POST /v1/customers",
79
+ "customers.list": "GET /v1/customers"
80
+ }
81
+ }
58
82
  },
59
83
  {
60
84
  id: "twilio",
@@ -66,12 +90,23 @@ var providers_default = [
66
90
  "calls.create": "POST /2010-04-01/Accounts/{AccountSid}/Calls.json"
67
91
  },
68
92
  docsUrl: "https://www.twilio.com/docs/usage/api",
69
- changelogUrl: "https://www.twilio.com/en-us/changelog"
93
+ changelogUrl: "https://www.twilio.com/en-us/changelog",
94
+ python: {
95
+ packages: [
96
+ { pypi: "twilio", module: "twilio" }
97
+ ],
98
+ methods: {
99
+ "messages.create": "POST /2010-04-01/Accounts/{AccountSid}/Messages.json",
100
+ "messages.list": "GET /2010-04-01/Accounts/{AccountSid}/Messages.json",
101
+ "calls.create": "POST /2010-04-01/Accounts/{AccountSid}/Calls.json",
102
+ "conferences.list": "GET /2010-04-01/Accounts/{AccountSid}/Conferences.json"
103
+ }
104
+ }
70
105
  },
71
106
  {
72
107
  id: "shopify",
73
108
  name: "Shopify",
74
- hosts: ["*.myshopify.com"],
109
+ hosts: ["*.myshopify.com", "discover.shopifyapps.com"],
75
110
  sdkPackages: ["@shopify/shopify-api", "@shopify/admin-api-client", "shopify-api-node"],
76
111
  docsUrl: "https://shopify.dev/docs/api/admin",
77
112
  changelogUrl: "https://shopify.dev/changelog"
@@ -98,7 +133,18 @@ var providers_default = [
98
133
  hosts: ["api.hubapi.com", "api.hubspot.com"],
99
134
  sdkPackages: ["@hubspot/api-client"],
100
135
  docsUrl: "https://developers.hubspot.com/docs/api/overview",
101
- changelogUrl: "https://developers.hubspot.com/changelog"
136
+ changelogUrl: "https://developers.hubspot.com/changelog",
137
+ python: {
138
+ packages: [
139
+ { pypi: "hubspot-api-client", module: "hubspot" }
140
+ ],
141
+ methods: {
142
+ "crm.contacts.basic_api.get_page": "GET /crm/v3/objects/contacts",
143
+ "crm.contacts.basic_api.create": "POST /crm/v3/objects/contacts",
144
+ "crm.deals.basic_api.get_page": "GET /crm/v3/objects/deals",
145
+ "crm.pipelines.pipelines_api.get_all": "GET /crm/v3/pipelines/{objectType}"
146
+ }
147
+ }
102
148
  },
103
149
  {
104
150
  id: "slack",
@@ -114,7 +160,12 @@ var providers_default = [
114
160
  "files.uploadV2": "POST /api/files.uploadV2"
115
161
  },
116
162
  docsUrl: "https://api.slack.com/methods",
117
- changelogUrl: "https://api.slack.com/changelog"
163
+ changelogUrl: "https://api.slack.com/changelog",
164
+ python: {
165
+ packages: [
166
+ { pypi: "slack-sdk", module: "slack_sdk" }
167
+ ]
168
+ }
118
169
  },
119
170
  {
120
171
  id: "openai",
@@ -132,7 +183,27 @@ var providers_default = [
132
183
  "files.create": "POST /v1/files"
133
184
  },
134
185
  docsUrl: "https://platform.openai.com/docs/api-reference",
135
- changelogUrl: "https://platform.openai.com/docs/changelog"
186
+ changelogUrl: "https://platform.openai.com/docs/changelog",
187
+ python: {
188
+ packages: [
189
+ { pypi: "openai", module: "openai" }
190
+ ],
191
+ methods: {
192
+ "chat.completions.create": "POST /v1/chat/completions",
193
+ "responses.create": "POST /v1/responses",
194
+ "models.list": "GET /v1/models",
195
+ "embeddings.create": "POST /v1/embeddings",
196
+ "audio.translations.create": "POST /v1/audio/translations",
197
+ "audio.transcriptions.create": "POST /v1/audio/transcriptions",
198
+ "images.generate": "POST /v1/images/generations",
199
+ "beta.assistants.create": "POST /v1/assistants",
200
+ "beta.assistants.list": "GET /v1/assistants",
201
+ "beta.threads.create": "POST /v1/threads",
202
+ "beta.threads.runs.create": "POST /v1/threads/{thread_id}/runs",
203
+ "evals.create": "POST /v1/evals",
204
+ "evals.list": "GET /v1/evals"
205
+ }
206
+ }
136
207
  },
137
208
  {
138
209
  id: "anthropic",
@@ -146,7 +217,19 @@ var providers_default = [
146
217
  "models.list": "GET /v1/models"
147
218
  },
148
219
  docsUrl: "https://docs.claude.com/en/api",
149
- changelogUrl: "https://docs.claude.com/en/release-notes/api"
220
+ changelogUrl: "https://docs.claude.com/en/release-notes/api",
221
+ python: {
222
+ packages: [
223
+ { pypi: "anthropic", module: "anthropic" }
224
+ ],
225
+ methods: {
226
+ "messages.create": "POST /v1/messages",
227
+ "messages.stream": "POST /v1/messages",
228
+ "messages.count_tokens": "POST /v1/messages/count_tokens",
229
+ "messages.batches.create": "POST /v1/messages/batches",
230
+ "models.list": "GET /v1/models"
231
+ }
232
+ }
150
233
  },
151
234
  {
152
235
  id: "salesforce",
@@ -357,7 +440,12 @@ var ProviderSchema = z.object({
357
440
  /** SDK method chain (after the client, without a leading "rest.") to the HTTP endpoint it calls. */
358
441
  sdkMethods: z.record(z.string(), EndpointRefSchema).optional(),
359
442
  docsUrl: z.url(),
360
- changelogUrl: z.url().optional()
443
+ changelogUrl: z.url().optional(),
444
+ /** The provider's Python SDK: PyPI package and import module, and SDK method chain → HTTP endpoint. */
445
+ python: z.object({
446
+ packages: z.array(z.object({ pypi: z.string().min(1), module: z.string().min(1) })),
447
+ methods: z.record(z.string(), EndpointRefSchema).optional()
448
+ }).optional()
361
449
  });
362
450
  var ProviderRegistrySchema = z.array(ProviderSchema).refine((ps) => new Set(ps.map((p) => p.id)).size === ps.length, "provider ids must be unique");
363
451
  var cached;
@@ -365,6 +453,9 @@ function loadProviders() {
365
453
  cached ??= ProviderRegistrySchema.parse(providers_default);
366
454
  return cached;
367
455
  }
456
+ function getProvider(id) {
457
+ return loadProviders().find((p) => p.id === id);
458
+ }
368
459
  function providerForHost(host) {
369
460
  const h = host.toLowerCase().replace(/:\d+$/, "");
370
461
  let best;
@@ -381,6 +472,14 @@ function providerForHost(host) {
381
472
  function providerForPackage(name) {
382
473
  return loadProviders().find((p) => p.sdkPackages.includes(name));
383
474
  }
475
+ function providerForPythonModule(module) {
476
+ const top = module.split(".")[0];
477
+ return loadProviders().find((p) => p.python?.packages.some((pkg) => pkg.module === top));
478
+ }
479
+ function providerForPythonPackage(name) {
480
+ const norm = (s) => s.toLowerCase().replace(/[-_.]+/g, "-");
481
+ return loadProviders().find((p) => p.python?.packages.some((pkg) => norm(pkg.pypi) === norm(name)));
482
+ }
384
483
  function sdkEndpoint(provider, chain) {
385
484
  const methods = provider.sdkMethods ?? {};
386
485
  const key = chain.replace(/^rest\./, "");
@@ -516,11 +615,26 @@ var TriggerSchema = z3.object({
516
615
  enums: z3.array(z3.object({ field: z3.string() })).optional(),
517
616
  /** Affected when the request sets this header (optionally to one of these values). */
518
617
  headers: z3.array(z3.object({ name: z3.string(), values: z3.array(z3.string()).optional() })).optional(),
519
- /** Affected when the request sets this query parameter (optionally to a number below `below`). */
520
- query: z3.array(z3.object({ name: z3.string(), below: z3.number().optional() })).optional(),
618
+ /**
619
+ * Affected when the request sets this query parameter: optionally to a number below `below`, or to
620
+ * one of `values` (a comma-separated value such as `metric=a,b` matches when any item does).
621
+ */
622
+ query: z3.array(z3.object({ name: z3.string(), below: z3.number().optional(), values: z3.array(z3.string()).optional() })).optional(),
521
623
  /** Every call to the endpoint is affected (sunsets, auth and scope changes). */
522
624
  any: z3.boolean().optional()
523
625
  }).refine((t) => Object.values(t).some((v) => v !== void 0 && v !== false), "trigger needs at least one condition");
626
+ var FixSchema = z3.object({
627
+ /** Replace this part of the request path, for example "/v21.0/" with "/v26.0/". */
628
+ path: z3.object({ from: z3.string().min(1), to: z3.string().min(1) }).optional(),
629
+ /** Send this query parameter with this value, for example minorversion=75. */
630
+ query: z3.object({ name: z3.string().min(1), value: z3.string() }).optional(),
631
+ /** Send this request header with this value, for example Stripe-Version. */
632
+ header: z3.object({ name: z3.string().min(1), value: z3.string() }).optional(),
633
+ /** Response fields that were renamed: the code reads the new name, falling back to the old one. */
634
+ fields: z3.array(z3.object({ from: z3.string().min(1), to: z3.string().min(1) })).optional(),
635
+ /** Enum values that were renamed: comparisons with an old value also accept the new one. */
636
+ enums: z3.array(z3.object({ field: z3.string().min(1), values: z3.array(z3.object({ from: z3.string(), to: z3.string() })).min(1) })).optional()
637
+ }).refine((f) => Object.values(f).some((v) => v !== void 0), "fix needs at least one change");
524
638
  var AdvisorySchema = z3.object({
525
639
  id: z3.string().regex(/^[a-z0-9-]+$/),
526
640
  provider: z3.string(),
@@ -538,6 +652,8 @@ var AdvisorySchema = z3.object({
538
652
  severity: z3.enum(SEVERITIES),
539
653
  migrationNote: z3.string().min(1),
540
654
  recipeId: z3.string().optional(),
655
+ /** What `recipeId: "apply-fix"` changes. */
656
+ fix: FixSchema.optional(),
541
657
  sources: z3.array(z3.url()).min(1),
542
658
  /** "verified": checked against a primary or two independent sources. "unverified": secondary reports only. */
543
659
  status: z3.enum(["verified", "unverified"]),
@@ -646,8 +762,12 @@ function triggerReasons(site, trigger) {
646
762
  for (const q of trigger.query ?? []) {
647
763
  const p = s.query.find((x) => x.name === q.name);
648
764
  if (!p) continue;
649
- if (q.below === void 0) reasons.push(`sends ${p.name}=${p.value ?? "(dynamic)"}`);
650
- else if (p.value !== null && Number(p.value) < q.below) reasons.push(`sends ${p.name}=${p.value} (below ${q.below})`);
765
+ if (q.below !== void 0) {
766
+ if (p.value !== null && Number(p.value) < q.below) reasons.push(`sends ${p.name}=${p.value} (below ${q.below})`);
767
+ } else if (q.values) {
768
+ const hit = p.value === null ? [] : p.value.split(",").map((v) => v.trim()).filter((v) => q.values.includes(v));
769
+ if (hit.length) reasons.push(`sends ${p.name}=${hit.join(",")}`);
770
+ } else reasons.push(`sends ${p.name}=${p.value ?? "(dynamic)"}`);
651
771
  }
652
772
  if (trigger.any) reasons.push(`calls ${describe(site)}`);
653
773
  return reasons;
@@ -843,7 +963,8 @@ function fetchStyle(ctx, call, lib, urlArg, optsArg, verb, baseURL) {
843
963
  if (opts) method = methodFrom(ctx, propertyValue(opts, "method")) ?? (propertyValue(opts, "method") ? null : "GET");
844
964
  else method = null;
845
965
  }
846
- return httpSite(ctx, call, lib, joinUrl(baseURL, evalString(ctx, urlArg)), method, opts ? optsArg : void 0);
966
+ const params = opts && (lib === "got" || lib === "ky") ? resolveObject(ctx, propertyValue(opts, "searchParams")) : void 0;
967
+ return httpSite(ctx, call, lib, joinUrl(baseURL, evalString(ctx, urlArg)), method, opts ? optsArg : void 0, params);
847
968
  }
848
969
  function axiosStyle(ctx, call, verb, baseURL) {
849
970
  const args = call.getArguments();
@@ -1136,6 +1257,7 @@ var NOT_FIELDS = /* @__PURE__ */ new Set([
1136
1257
  "valueOf",
1137
1258
  "toJSON"
1138
1259
  ]);
1260
+ var ITEM_CALLBACKS = { map: 0, flatMap: 0, filter: 0, forEach: 0, find: 0, findIndex: 0, some: 0, every: 0, reduce: 1 };
1139
1261
  var MAX_DEPTH = 4;
1140
1262
  var Collector = class {
1141
1263
  fields = /* @__PURE__ */ new Set();
@@ -1250,7 +1372,7 @@ function use(ref, loc, env, c, depth) {
1250
1372
  function interpret(env, loc, access, top, c, depth, compare) {
1251
1373
  const full = [...loc.path, ...access];
1252
1374
  if (loc.space === "body" || env === "body") {
1253
- body(full, top, c, compare);
1375
+ body(full, top, c, compare, env, depth);
1254
1376
  return;
1255
1377
  }
1256
1378
  const [first, ...rest] = full;
@@ -1276,20 +1398,37 @@ function interpret(env, loc, access, top, c, depth, compare) {
1276
1398
  } else if (h && h !== "get") c.responseHeaders.add(h.toLowerCase());
1277
1399
  return;
1278
1400
  }
1279
- if (env === "axios" && first === "data") return body(rest, top, c, compare);
1280
- if (env === "got" && first === "body") return body(rest, top, c, compare);
1401
+ if (env === "axios" && first === "data") return body(rest, top, c, compare, env, depth);
1402
+ if (env === "got" && first === "body") return body(rest, top, c, compare, env, depth);
1281
1403
  const bodyCall = (env === "fetch" || env === "got") && rest.length === 0 && (first === "json" || first === "text") || env === "undici" && first === "body" && rest.length === 1 && (rest[0] === "json" || rest[0] === "text");
1282
1404
  if (bodyCall && isCall()) {
1283
1405
  bindFrom(top.getParent(), { space: "body", path: [] }, env, c, depth + 1);
1284
1406
  }
1285
1407
  }
1286
- function body(path13, top, c, compare) {
1408
+ function body(path13, top, c, compare, env, depth) {
1287
1409
  const cut = path13.findIndex((p) => NOT_FIELDS.has(p));
1288
1410
  const fieldPath = cut >= 0 ? path13.slice(0, cut) : path13;
1289
1411
  if (fieldPath.length === 0) return;
1290
1412
  const field = fieldPath.join(".").replace(/\.\[\]/g, "[]");
1291
1413
  c.fields.add(field);
1292
1414
  if (compare && cut < 0) for (const s of comparedLiterals(top).strs) c.addEnum(field, s);
1415
+ if (depth >= MAX_DEPTH) return;
1416
+ const items = { space: "body", path: [...fieldPath, "[]"] };
1417
+ const method = cut >= 0 && cut === path13.length - 1 ? path13[cut] : void 0;
1418
+ const call = top.getParent();
1419
+ if (method && method in ITEM_CALLBACKS && Node2.isCallExpression(call) && call.getExpression() === top) {
1420
+ const cb = call.getArguments()[0];
1421
+ const param = cb && (Node2.isArrowFunction(cb) || Node2.isFunctionExpression(cb)) ? cb.getParameters()[ITEM_CALLBACKS[method]] : void 0;
1422
+ if (param) bindName(param.getNameNode(), items, param, env, c, depth + 1);
1423
+ if (method === "find") bindFrom(call, items, env, c, depth + 1);
1424
+ return;
1425
+ }
1426
+ const loop = top.getParent();
1427
+ if (cut < 0 && Node2.isForOfStatement(loop) && loop.getExpression() === top) {
1428
+ const list = loop.getInitializer();
1429
+ const decl = Node2.isVariableDeclarationList(list) ? list.getDeclarations()[0] : void 0;
1430
+ if (decl) bindName(decl.getNameNode(), items, decl, env, c, depth + 1);
1431
+ }
1293
1432
  }
1294
1433
  function comparedLiterals(node) {
1295
1434
  const nums = [];
@@ -1323,10 +1462,14 @@ function comparedLiterals(node) {
1323
1462
  import { readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
1324
1463
  import path3 from "path";
1325
1464
  var SOURCE_FILE = /\.(c|m)?[jt]sx?$/;
1326
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage", "vendor", "bower_components"]);
1465
+ var PYTHON_FILE = /\.py$/;
1466
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage", "vendor", "bower_components", "venv", "__pycache__", "site-packages"]);
1467
+ var PYTHON_MANIFEST = /^(requirements[\w.-]*\.txt|pyproject\.toml)$/;
1327
1468
  function listRepoFiles(root) {
1328
1469
  const sources = [];
1329
1470
  const manifests = [];
1471
+ const python = [];
1472
+ const pythonManifests = [];
1330
1473
  const walk2 = (dir) => {
1331
1474
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
1332
1475
  if (entry.name.startsWith(".")) continue;
@@ -1336,11 +1479,13 @@ function listRepoFiles(root) {
1336
1479
  } else if (entry.isFile()) {
1337
1480
  if (entry.name === "package.json") manifests.push(full);
1338
1481
  else if (SOURCE_FILE.test(entry.name) && !entry.name.endsWith(".d.ts")) sources.push(full);
1482
+ else if (PYTHON_FILE.test(entry.name)) python.push(full);
1483
+ else if (PYTHON_MANIFEST.test(entry.name)) pythonManifests.push(full);
1339
1484
  }
1340
1485
  }
1341
1486
  };
1342
1487
  walk2(root);
1343
- return { sources: sources.sort(), manifests: manifests.sort() };
1488
+ return { sources: sources.sort(), manifests: manifests.sort(), python: python.sort(), pythonManifests: pythonManifests.sort() };
1344
1489
  }
1345
1490
  function declaredProviders(root, manifests) {
1346
1491
  const found = [];
@@ -1366,6 +1511,530 @@ function declaredProviders(root, manifests) {
1366
1511
  function toPosix(p) {
1367
1512
  return p.split(path3.sep).join("/");
1368
1513
  }
1514
+ function declaredPythonProviders(root, manifests) {
1515
+ const found = [];
1516
+ for (const manifest of manifests) {
1517
+ let text;
1518
+ try {
1519
+ text = readFileSync2(manifest, "utf8");
1520
+ } catch {
1521
+ continue;
1522
+ }
1523
+ const names = /* @__PURE__ */ new Set();
1524
+ if (manifest.endsWith(".txt")) {
1525
+ for (const line of text.split(/\r?\n/)) {
1526
+ const m = /^\s*([A-Za-z0-9][A-Za-z0-9._-]*)/.exec(line.replace(/#.*/, ""));
1527
+ if (m && !line.trim().startsWith("-")) names.add(m[1]);
1528
+ }
1529
+ } else {
1530
+ let inTable = false;
1531
+ let inList = false;
1532
+ for (const line of text.split(/\r?\n/)) {
1533
+ const t = line.replace(/#.*/, "").trim();
1534
+ if (/^\[/.test(t)) {
1535
+ inTable = /dependencies\]$/.test(t);
1536
+ inList = false;
1537
+ continue;
1538
+ }
1539
+ if (/^(dependencies|optional-dependencies|dev-dependencies)\s*=\s*\[/.test(t) || /^[\w-]+\s*=\s*\[/.test(t)) inList = true;
1540
+ if (inList) for (const m of t.matchAll(/["']([A-Za-z0-9][A-Za-z0-9._-]*)/g)) names.add(m[1]);
1541
+ if (inList && t.includes("]")) inList = false;
1542
+ const kv = inTable ? /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*=/.exec(t) : null;
1543
+ if (kv) names.add(kv[1]);
1544
+ }
1545
+ }
1546
+ for (const name of [...names].sort()) {
1547
+ const provider = providerForPythonPackage(name);
1548
+ if (provider) found.push({ provider: provider.id, package: name, manifest: toPosix(path3.relative(root, manifest)) });
1549
+ }
1550
+ }
1551
+ return found;
1552
+ }
1553
+
1554
+ // packages/extractor/src/python.ts
1555
+ import { existsSync } from "fs";
1556
+ import { createRequire } from "module";
1557
+ import { fileURLToPath } from "url";
1558
+ import { Language, Parser } from "web-tree-sitter";
1559
+ var parser;
1560
+ async function loadPythonParser() {
1561
+ if (parser) return;
1562
+ await Parser.init();
1563
+ const bundled = fileURLToPath(new URL("./tree-sitter-python.wasm", import.meta.url));
1564
+ const wasm = existsSync(bundled) ? bundled : createRequire(import.meta.url).resolve("tree-sitter-python/tree-sitter-python.wasm");
1565
+ const language = await Language.load(wasm);
1566
+ const p = new Parser();
1567
+ p.setLanguage(language);
1568
+ parser = p;
1569
+ }
1570
+ function pythonParserLoaded() {
1571
+ return parser !== void 0;
1572
+ }
1573
+ var HTTP_LIBS = /* @__PURE__ */ new Set(["requests", "httpx", "aiohttp"]);
1574
+ var VERBS2 = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
1575
+ function extractPython(source, file) {
1576
+ if (!parser) throw new Error("call loadPythonParser() before extracting Python");
1577
+ const tree = parser.parse(source);
1578
+ if (!tree) return [];
1579
+ try {
1580
+ const root = tree.rootNode;
1581
+ const ctx = { names: /* @__PURE__ */ new Map(), consts: /* @__PURE__ */ new Map() };
1582
+ collectImports2(root, ctx);
1583
+ collectConsts2(root, ctx);
1584
+ collectClients2(root, ctx);
1585
+ const sites = [];
1586
+ for (const call of root.descendantsOfType("call")) {
1587
+ if (!call) continue;
1588
+ const site = httpCall(ctx, call) ?? sdkCall(ctx, call);
1589
+ if (!site) continue;
1590
+ site.file = file;
1591
+ site.line = call.startPosition.row + 1;
1592
+ site.column = call.startPosition.column + 1;
1593
+ addResponseSurface2(call, site);
1594
+ sites.push(site);
1595
+ }
1596
+ return sites;
1597
+ } finally {
1598
+ tree.delete();
1599
+ }
1600
+ }
1601
+ function collectImports2(root, ctx) {
1602
+ for (const st of root.descendantsOfType(["import_statement", "import_from_statement"])) {
1603
+ if (!st) continue;
1604
+ if (st.type === "import_statement") {
1605
+ for (const item of st.namedChildren) {
1606
+ if (!item) continue;
1607
+ const dotted = item.type === "aliased_import" ? item.childForFieldName("name") : item;
1608
+ const alias = item.type === "aliased_import" ? item.childForFieldName("alias")?.text : dotted?.text.split(".")[0];
1609
+ if (!dotted || !alias) continue;
1610
+ bindModule(ctx, alias, dotted.text);
1611
+ }
1612
+ } else {
1613
+ const module = st.childForFieldName("module_name")?.text;
1614
+ if (!module) continue;
1615
+ for (const item of st.childrenForFieldName("name")) {
1616
+ if (!item) continue;
1617
+ const name = item.type === "aliased_import" ? item.childForFieldName("name")?.text : item.text;
1618
+ const alias = item.type === "aliased_import" ? item.childForFieldName("alias")?.text : item.text;
1619
+ if (name && alias) bindModule(ctx, alias, `${module}.${name}`);
1620
+ }
1621
+ }
1622
+ }
1623
+ }
1624
+ function bindModule(ctx, alias, dotted) {
1625
+ const top = dotted.split(".")[0];
1626
+ if (HTTP_LIBS.has(top)) {
1627
+ ctx.names.set(alias, { kind: "http-module", lib: top });
1628
+ return;
1629
+ }
1630
+ const provider = providerForPythonModule(top);
1631
+ if (provider) ctx.names.set(alias, { kind: "sdk-module", provider, module: dotted });
1632
+ }
1633
+ function collectConsts2(root, ctx) {
1634
+ for (const st of root.namedChildren) {
1635
+ const assign = st?.type === "expression_statement" ? st.namedChildren[0] : void 0;
1636
+ if (assign?.type !== "assignment") continue;
1637
+ const left = assign.childForFieldName("left");
1638
+ const right = assign.childForFieldName("right");
1639
+ if (left?.type === "identifier" && right) ctx.consts.set(left.text, right);
1640
+ }
1641
+ }
1642
+ function collectClients2(root, ctx) {
1643
+ const visit = (target, value2) => {
1644
+ const call = unwrap2(value2);
1645
+ if (call.type !== "call") return;
1646
+ const fn = call.childForFieldName("function");
1647
+ const chain = fn ? chainOf2(fn) : void 0;
1648
+ if (!chain) return;
1649
+ const base = ctx.names.get(chain.root);
1650
+ if (!base) return;
1651
+ if (base.kind === "http-module" || base.kind === "http-client") {
1652
+ const ctor = chain.names.at(-1);
1653
+ if (!ctor || !/^(Session|Client|AsyncClient|ClientSession)$/.test(ctor)) return;
1654
+ const args = call.childForFieldName("arguments");
1655
+ const baseUrl = evalString2(ctx, keyword(args, "base_url"));
1656
+ ctx.names.set(target, { kind: "http-client", lib: base.lib, ...baseUrl ? { baseUrl } : {}, headers: dictEntries(ctx, keyword(args, "headers")) });
1657
+ return;
1658
+ }
1659
+ if (base.kind === "sdk-module" && !sdkEndpoint2(base.provider, [...modulePath(base), ...chain.names])) {
1660
+ ctx.names.set(target, { kind: "sdk-client", provider: base.provider, module: base.module });
1661
+ }
1662
+ };
1663
+ for (const assign of root.descendantsOfType("assignment")) {
1664
+ const left = assign?.childForFieldName("left");
1665
+ const right = assign?.childForFieldName("right");
1666
+ if (left && right && (left.type === "identifier" || left.type === "attribute")) visit(left.text, right);
1667
+ }
1668
+ for (const item of root.descendantsOfType("as_pattern")) {
1669
+ const value2 = item?.namedChildren[0];
1670
+ const alias = item?.childForFieldName("alias")?.namedChildren[0] ?? item?.namedChildren.at(-1);
1671
+ if (value2 && alias && alias.type === "identifier") visit(alias.text, value2);
1672
+ }
1673
+ }
1674
+ function httpCall(ctx, call) {
1675
+ const fn = call.childForFieldName("function");
1676
+ const chain = fn ? chainOf2(fn) : void 0;
1677
+ if (!chain || chain.names.length === 0) {
1678
+ return void 0;
1679
+ }
1680
+ const binding = ctx.names.get(chain.root) ?? ctx.names.get(chain.prefix(chain.names.length - 1));
1681
+ const member = chain.names.at(-1);
1682
+ if (!binding || binding.kind !== "http-module" && binding.kind !== "http-client") return void 0;
1683
+ if (binding.kind === "http-module" && chain.names.length !== 1) return void 0;
1684
+ const args = call.childForFieldName("arguments");
1685
+ const positional = (args?.namedChildren ?? []).filter((a) => a && a.type !== "keyword_argument");
1686
+ let method;
1687
+ let urlNode;
1688
+ if (VERBS2.has(member)) {
1689
+ method = member.toUpperCase();
1690
+ urlNode = positional[0] ?? keyword(args, "url");
1691
+ } else if (member === "request") {
1692
+ const m = evalString2(ctx, positional[0] ?? keyword(args, "method"))?.toUpperCase();
1693
+ method = m && HTTP_METHODS.includes(m) ? m : null;
1694
+ urlNode = positional[1] ?? keyword(args, "url");
1695
+ } else return void 0;
1696
+ const base = binding.kind === "http-client" ? binding.baseUrl : void 0;
1697
+ const { host, path: path13, query } = parseUrl(joinUrl2(base, evalString2(ctx, urlNode)));
1698
+ const surface = emptySurface2();
1699
+ surface.headers = [...binding.kind === "http-client" ? binding.headers : [], ...dictEntries(ctx, keyword(args, "headers"))].map((h) => ({ ...h, name: h.name.toLowerCase() }));
1700
+ surface.query = [...query, ...dictEntries(ctx, keyword(args, "params"))];
1701
+ const provider = host ? providerForHost(host)?.id ?? null : null;
1702
+ return { file: "", line: 0, column: 0, provider, kind: "http", client: binding.lib, method, host, endpoint: path13, resolved: Boolean(provider && path13 && method), surface };
1703
+ }
1704
+ function sdkCall(ctx, call) {
1705
+ const fn = call.childForFieldName("function");
1706
+ const chain = fn ? chainOf2(fn) : void 0;
1707
+ if (!chain || chain.names.length === 0) return void 0;
1708
+ const binding = ctx.names.get(chain.root);
1709
+ if (!binding || binding.kind !== "sdk-module" && binding.kind !== "sdk-client") return void 0;
1710
+ const names = binding.kind === "sdk-module" ? [...modulePath(binding), ...chain.names] : chain.names;
1711
+ const ep = sdkEndpoint2(binding.provider, names);
1712
+ if (!ep && (binding.kind === "sdk-module" || names.length < 2)) return void 0;
1713
+ const surface = emptySurface2();
1714
+ const host = binding.provider.hosts.find((h) => !h.startsWith("*.")) ?? null;
1715
+ return {
1716
+ file: "",
1717
+ line: 0,
1718
+ column: 0,
1719
+ provider: binding.provider.id,
1720
+ kind: "sdk",
1721
+ client: binding.module.split(".")[0],
1722
+ operation: names.join("."),
1723
+ method: ep?.method ?? null,
1724
+ host,
1725
+ endpoint: ep?.path ?? null,
1726
+ resolved: Boolean(ep),
1727
+ surface
1728
+ };
1729
+ }
1730
+ function modulePath(b) {
1731
+ return b.module.split(".").slice(1);
1732
+ }
1733
+ function sdkEndpoint2(provider, names) {
1734
+ let op = names.join(".");
1735
+ if (provider.id === "stripe") op = op.replace(/^v1\./, "");
1736
+ if (provider.id === "slack") {
1737
+ const last = names.at(-1);
1738
+ if (names.length !== 1 || !last || !/^[a-z]+(_[a-zA-Z0-9]+)+$/.test(last)) return void 0;
1739
+ if (last === "files_upload_v2") return { method: "POST", path: "/api/files.completeUploadExternal" };
1740
+ return { method: "POST", path: `/api/${last.replace(/_/g, ".")}` };
1741
+ }
1742
+ const ref = getProvider(provider.id)?.python?.methods?.[op];
1743
+ if (!ref) return void 0;
1744
+ const [method, path13] = ref.split(" ");
1745
+ return { method, path: path13 };
1746
+ }
1747
+ var NOT_FIELDS2 = /* @__PURE__ */ new Set([
1748
+ "keys",
1749
+ "values",
1750
+ "items",
1751
+ "append",
1752
+ "extend",
1753
+ "lower",
1754
+ "upper",
1755
+ "strip",
1756
+ "split",
1757
+ "startswith",
1758
+ "endswith",
1759
+ "replace",
1760
+ "format",
1761
+ "encode",
1762
+ "decode",
1763
+ "count",
1764
+ "index",
1765
+ "copy",
1766
+ "pop",
1767
+ "update",
1768
+ "sort"
1769
+ ]);
1770
+ var SAME_DATA = /* @__PURE__ */ new Set(["to_dict", "to_dict_recursive", "model_dump", "dict"]);
1771
+ var MAX_DEPTH2 = 4;
1772
+ var Collector2 = class {
1773
+ fields = /* @__PURE__ */ new Set();
1774
+ statusCodes = /* @__PURE__ */ new Set();
1775
+ checksOk = false;
1776
+ enums = /* @__PURE__ */ new Map();
1777
+ responseHeaders = /* @__PURE__ */ new Set();
1778
+ };
1779
+ function addResponseSurface2(call, site) {
1780
+ const c = new Collector2();
1781
+ const envelope = site.kind === "sdk" ? "body" : "response";
1782
+ bindFrom2(call, { space: envelope, path: [] }, site.client, c, 0);
1783
+ const s = site.surface;
1784
+ s.fields = [...c.fields].sort();
1785
+ s.statusCodes = [...c.statusCodes].sort((a, b) => a - b);
1786
+ s.checksOk = c.checksOk;
1787
+ s.enums = [...c.enums].map(([field, values]) => ({ field, values: [...values].sort() })).sort((a, b) => a.field.localeCompare(b.field));
1788
+ s.responseHeaders = [...c.responseHeaders].sort();
1789
+ }
1790
+ function bindFrom2(expr, loc, lib, c, depth) {
1791
+ if (depth > MAX_DEPTH2) return;
1792
+ let cur = expr;
1793
+ while (cur.parent && (cur.parent.type === "await" || cur.parent.type === "parenthesized_expression")) cur = cur.parent;
1794
+ const parent = cur.parent;
1795
+ if (!parent) return;
1796
+ if (parent.type === "assignment" && parent.childForFieldName("right")?.id === cur.id) {
1797
+ const left = parent.childForFieldName("left");
1798
+ if (left?.type === "identifier") bindName2(left.text, parent, loc, lib, c, depth);
1799
+ return;
1800
+ }
1801
+ if (parent.type === "as_pattern" && parent.namedChildren[0]?.id === cur.id) {
1802
+ const alias = parent.childForFieldName("alias")?.namedChildren[0] ?? parent.namedChildren.at(-1);
1803
+ if (alias?.type === "identifier") bindName2(alias.text, parent, loc, lib, c, depth);
1804
+ return;
1805
+ }
1806
+ use2(cur, loc, lib, c, depth);
1807
+ }
1808
+ function bindName2(name, decl, loc, lib, c, depth) {
1809
+ for (const ref of referencesOf2(name, decl)) use2(ref, loc, lib, c, depth);
1810
+ }
1811
+ function referencesOf2(name, decl) {
1812
+ let scope = decl.parent;
1813
+ while (scope && scope.type !== "function_definition" && scope.type !== "lambda" && scope.type !== "module") scope = scope.parent;
1814
+ if (!scope) return [];
1815
+ return scope.descendantsOfType("identifier").filter((id) => {
1816
+ if (!id || id.text !== name || id.startIndex < decl.endIndex) return false;
1817
+ const p = id.parent;
1818
+ if (p?.type === "attribute" && p.childForFieldName("attribute")?.id === id.id) return false;
1819
+ if (p?.type === "keyword_argument" && p.childForFieldName("name")?.id === id.id) return false;
1820
+ if (p?.type === "assignment" && p.childForFieldName("left")?.id === id.id) return false;
1821
+ return true;
1822
+ });
1823
+ }
1824
+ function use2(ref, loc, lib, c, depth) {
1825
+ const access = [];
1826
+ let cur = ref;
1827
+ for (; ; ) {
1828
+ const p = cur.parent;
1829
+ if (!p) break;
1830
+ if (p.type === "attribute" && p.childForFieldName("object")?.id === cur.id) {
1831
+ const name = p.childForFieldName("attribute")?.text ?? "";
1832
+ const call = p.parent?.type === "call" && p.parent.childForFieldName("function")?.id === p.id ? p.parent : void 0;
1833
+ if (call && name === "get" && (loc.space === "body" || access.length > 0)) {
1834
+ const key = stringValue(call.childForFieldName("arguments")?.namedChildren[0] ?? void 0);
1835
+ if (key === void 0) break;
1836
+ access.push(key);
1837
+ cur = call;
1838
+ continue;
1839
+ }
1840
+ if (call && SAME_DATA.has(name) && loc.space === "body") {
1841
+ cur = call;
1842
+ continue;
1843
+ }
1844
+ access.push(name);
1845
+ cur = p;
1846
+ } else if (p.type === "subscript" && p.childForFieldName("value")?.id === cur.id) {
1847
+ const key = stringValue(p.childForFieldName("subscript") ?? void 0);
1848
+ access.push(key ?? "[]");
1849
+ cur = p;
1850
+ } else if (p.type === "parenthesized_expression" || p.type === "await") {
1851
+ cur = p;
1852
+ } else break;
1853
+ }
1854
+ interpret2(loc, access, cur, lib, c, depth);
1855
+ }
1856
+ function interpret2(loc, access, top, lib, c, depth) {
1857
+ const full = [...loc.path, ...access];
1858
+ if (loc.space === "body") return body2(full, top, lib, c, depth);
1859
+ const [first, ...rest] = full;
1860
+ if (first === void 0) return;
1861
+ const isCall = top.parent?.type === "call" && top.parent.childForFieldName("function")?.id === top.id;
1862
+ const statusKey = lib === "aiohttp" ? "status" : "status_code";
1863
+ if (first === statusKey && rest.length === 0) {
1864
+ for (const n of comparedLiterals2(top).nums) c.statusCodes.add(n);
1865
+ return;
1866
+ }
1867
+ if (first === "ok" && rest.length === 0 || first === "raise_for_status" && isCall) {
1868
+ c.checksOk = true;
1869
+ return;
1870
+ }
1871
+ if (first === "headers") {
1872
+ if (rest[0]) c.responseHeaders.add(rest[0].toLowerCase());
1873
+ return;
1874
+ }
1875
+ if (first === "json" && rest.length === 0 && isCall) bindFrom2(top.parent, { space: "body", path: [] }, lib, c, depth + 1);
1876
+ }
1877
+ function body2(path13, top, lib, c, depth) {
1878
+ const cut = path13.findIndex((p) => NOT_FIELDS2.has(p));
1879
+ const fieldPath = cut >= 0 ? path13.slice(0, cut) : path13;
1880
+ if (fieldPath.length === 0) {
1881
+ if (path13.length === 0 && depth < MAX_DEPTH2) iterate(top, { space: "body", path: [] }, lib, c, depth);
1882
+ return;
1883
+ }
1884
+ const field = fieldPath.join(".").replace(/\.\[\]/g, "[]");
1885
+ c.fields.add(field);
1886
+ if (cut < 0) {
1887
+ for (const s of comparedLiterals2(top).strs) {
1888
+ const set = c.enums.get(field) ?? /* @__PURE__ */ new Set();
1889
+ set.add(s);
1890
+ c.enums.set(field, set);
1891
+ }
1892
+ if (depth < MAX_DEPTH2) {
1893
+ const parent = top.parent;
1894
+ if (parent?.type === "assignment" && parent.childForFieldName("right")?.id === top.id) {
1895
+ const left = parent.childForFieldName("left");
1896
+ if (left?.type === "identifier") bindName2(left.text, parent, { space: "body", path: fieldPath }, lib, c, depth + 1);
1897
+ }
1898
+ iterate(top, { space: "body", path: fieldPath }, lib, c, depth);
1899
+ }
1900
+ }
1901
+ }
1902
+ function iterate(top, loc, lib, c, depth) {
1903
+ const p = top.parent;
1904
+ const items = { space: "body", path: [...loc.path, "[]"] };
1905
+ if (p?.type === "for_statement" && p.childForFieldName("right")?.id === top.id) {
1906
+ const left = p.childForFieldName("left");
1907
+ if (left?.type === "identifier") bindName2(left.text, left, items, lib, c, depth + 1);
1908
+ } else if (p?.type === "for_in_clause" && p.childForFieldName("right")?.id === top.id) {
1909
+ const left = p.childForFieldName("left");
1910
+ const comp = p.parent;
1911
+ if (left?.type === "identifier" && comp) {
1912
+ for (const id of comp.descendantsOfType("identifier")) {
1913
+ if (id && id.text === left.text && id.id !== left.id) use2(id, items, lib, c, depth + 1);
1914
+ }
1915
+ }
1916
+ }
1917
+ }
1918
+ function comparedLiterals2(node) {
1919
+ const nums = [];
1920
+ const strs = [];
1921
+ let cur = node;
1922
+ while (cur.parent?.type === "parenthesized_expression") cur = cur.parent;
1923
+ const p = cur.parent;
1924
+ if (p?.type !== "comparison_operator") return { nums, strs };
1925
+ const take = (n) => {
1926
+ if (!n) return;
1927
+ if (n.type === "integer") nums.push(Number(n.text));
1928
+ else if (n.type === "string") {
1929
+ const v = stringValue(n);
1930
+ if (v !== void 0) strs.push(v);
1931
+ } else if (n.type === "tuple" || n.type === "list" || n.type === "set") n.namedChildren.forEach(take);
1932
+ };
1933
+ for (const operand of p.namedChildren) if (operand && operand.id !== cur.id) take(operand);
1934
+ return { nums, strs };
1935
+ }
1936
+ function chainOf2(node) {
1937
+ const names = [];
1938
+ let cur = node;
1939
+ while (cur.type === "attribute") {
1940
+ names.unshift(cur.childForFieldName("attribute").text);
1941
+ cur = cur.childForFieldName("object");
1942
+ }
1943
+ if (cur.type !== "identifier") return void 0;
1944
+ const root = cur.text;
1945
+ return { root, names, prefix: (n) => [root, ...names.slice(0, n)].join(".") };
1946
+ }
1947
+ function unwrap2(n) {
1948
+ let cur = n;
1949
+ while (cur.type === "await" || cur.type === "parenthesized_expression") cur = cur.namedChildren[0] ?? cur;
1950
+ return cur;
1951
+ }
1952
+ function keyword(args, name) {
1953
+ for (const a of args?.namedChildren ?? []) {
1954
+ if (a?.type === "keyword_argument" && a.childForFieldName("name")?.text === name) return a.childForFieldName("value") ?? void 0;
1955
+ }
1956
+ return void 0;
1957
+ }
1958
+ function stringValue(n) {
1959
+ if (!n || n.type !== "string") return void 0;
1960
+ if (n.namedChildren.some((c) => c?.type === "interpolation")) return void 0;
1961
+ return n.namedChildren.filter((c) => c?.type === "string_content" || c?.type === "escape_sequence").map((c) => c.text).join("");
1962
+ }
1963
+ function evalString2(ctx, node, depth = 0) {
1964
+ if (!node || depth > 8) return void 0;
1965
+ const n = unwrap2(node);
1966
+ if (n.type === "string") {
1967
+ let out = "";
1968
+ for (const part of n.namedChildren) {
1969
+ if (!part) continue;
1970
+ if (part.type === "string_content" || part.type === "escape_sequence") out += part.text;
1971
+ else if (part.type === "interpolation") {
1972
+ const inner = part.childForFieldName("expression") ?? part.namedChildren[0];
1973
+ out += (inner && evalString2(ctx, inner, depth + 1)) ?? placeholder2(inner ?? void 0);
1974
+ }
1975
+ }
1976
+ return out;
1977
+ }
1978
+ if (n.type === "concatenated_string") return n.namedChildren.map((s) => evalString2(ctx, s ?? void 0, depth + 1) ?? "").join("");
1979
+ if (n.type === "binary_operator" && n.childForFieldName("operator")?.text === "+") {
1980
+ const l = evalString2(ctx, n.childForFieldName("left") ?? void 0, depth + 1);
1981
+ const r = evalString2(ctx, n.childForFieldName("right") ?? void 0, depth + 1);
1982
+ if (l === void 0 && r === void 0) return void 0;
1983
+ return (l ?? placeholder2(n.childForFieldName("left") ?? void 0)) + (r ?? placeholder2(n.childForFieldName("right") ?? void 0));
1984
+ }
1985
+ if (n.type === "identifier") {
1986
+ const init = ctx.consts.get(n.text);
1987
+ return init && init.id !== node.id ? evalString2(ctx, init, depth + 1) : void 0;
1988
+ }
1989
+ if (n.type === "call") {
1990
+ const fn = n.childForFieldName("function")?.text;
1991
+ const args = (n.childForFieldName("arguments")?.namedChildren ?? []).filter(Boolean);
1992
+ if ((fn === "os.getenv" || fn === "getenv" || fn === "os.environ.get") && args[1]) return evalString2(ctx, args[1], depth + 1);
1993
+ }
1994
+ return void 0;
1995
+ }
1996
+ function placeholder2(n) {
1997
+ if (!n) return "{}";
1998
+ const e = unwrap2(n);
1999
+ if (e.type === "identifier") return `{${e.text}}`;
2000
+ if (e.type === "attribute") return `{${e.childForFieldName("attribute")?.text ?? ""}}`;
2001
+ const args = e.type === "call" ? (e.childForFieldName("arguments")?.namedChildren ?? []).filter(Boolean) : [];
2002
+ if (args.length === 1) return placeholder2(args[0]);
2003
+ return "{}";
2004
+ }
2005
+ function dictEntries(ctx, node, depth = 0) {
2006
+ if (!node || depth > 3) return [];
2007
+ const n = unwrap2(node);
2008
+ if (n.type === "identifier") {
2009
+ const init = ctx.consts.get(n.text);
2010
+ return init ? dictEntries(ctx, init, depth + 1) : [];
2011
+ }
2012
+ if (n.type === "call" && n.childForFieldName("function")?.text === "dict") {
2013
+ return (n.childForFieldName("arguments")?.namedChildren ?? []).filter((a) => a?.type === "keyword_argument").map((a) => ({ name: a.childForFieldName("name").text, value: literal(ctx, a.childForFieldName("value") ?? void 0) }));
2014
+ }
2015
+ if (n.type !== "dictionary") return [];
2016
+ const out = [];
2017
+ for (const pair of n.namedChildren) {
2018
+ if (pair?.type !== "pair") continue;
2019
+ const key = stringValue(pair.childForFieldName("key") ?? void 0);
2020
+ if (key !== void 0) out.push({ name: key, value: literal(ctx, pair.childForFieldName("value") ?? void 0) });
2021
+ }
2022
+ return out;
2023
+ }
2024
+ function literal(ctx, n) {
2025
+ if (!n) return null;
2026
+ if (n.type === "integer" || n.type === "float") return n.text;
2027
+ if (n.type === "true" || n.type === "false") return n.text.toLowerCase();
2028
+ return evalString2(ctx, n) ?? null;
2029
+ }
2030
+ function joinUrl2(base, url) {
2031
+ if (url === void 0) return base;
2032
+ if (!base || /^[a-z]+:\/\//i.test(url)) return url;
2033
+ return base.replace(/\/+$/, "") + "/" + url.replace(/^\/+/, "");
2034
+ }
2035
+ function emptySurface2() {
2036
+ return { fields: [], statusCodes: [], checksOk: false, enums: [], headers: [], query: [], responseHeaders: [] };
2037
+ }
1369
2038
 
1370
2039
  // packages/extractor/src/index.ts
1371
2040
  var SCRIPT_KINDS = {
@@ -1380,7 +2049,7 @@ var SCRIPT_KINDS = {
1380
2049
  };
1381
2050
  function extractCallSites(rootDir) {
1382
2051
  const root = path4.resolve(rootDir);
1383
- const { sources, manifests } = listRepoFiles(root);
2052
+ const { sources, manifests, python, pythonManifests } = listRepoFiles(root);
1384
2053
  const project = new Project({
1385
2054
  useInMemoryFileSystem: true,
1386
2055
  skipFileDependencyResolution: true,
@@ -1399,12 +2068,28 @@ function extractCallSites(rootDir) {
1399
2068
  }
1400
2069
  }
1401
2070
  found.sort((a, b) => a.site.file.localeCompare(b.site.file) || a.site.line - b.site.line || a.site.column - b.site.column);
2071
+ const skipped = [];
2072
+ const pythonSites = [];
2073
+ for (const abs of python) {
2074
+ const rel = toPosix(path4.relative(root, abs));
2075
+ if (!pythonParserLoaded()) {
2076
+ skipped.push({ file: rel, reason: "the Python parser was not loaded" });
2077
+ continue;
2078
+ }
2079
+ try {
2080
+ pythonSites.push(...extractPython(readFileSync3(abs, "utf8"), rel));
2081
+ } catch (err) {
2082
+ skipped.push({ file: rel, reason: err instanceof Error ? err.message : String(err) });
2083
+ }
2084
+ }
2085
+ const callSites = [...found.map((f) => f.site), ...pythonSites].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column);
1402
2086
  return {
1403
2087
  root,
1404
- filesScanned: sources.length,
1405
- callSites: found.map((f) => f.site),
1406
- declared: declaredProviders(root, manifests),
1407
- found
2088
+ filesScanned: sources.length + python.length - skipped.length,
2089
+ callSites,
2090
+ declared: [...declaredProviders(root, manifests), ...declaredPythonProviders(root, pythonManifests)],
2091
+ found,
2092
+ skipped
1408
2093
  };
1409
2094
  }
1410
2095
 
@@ -1520,9 +2205,9 @@ function inferShape(res, opts) {
1520
2205
  else if (!VOLATILE_HEADERS.has(name) && !name.startsWith("x-ratelimit") && !name.startsWith("ratelimit")) headerNames.push(name);
1521
2206
  }
1522
2207
  const text = res.body ?? "";
1523
- let body2;
2208
+ let body3;
1524
2209
  if (text.trim() === "") {
1525
- body2 = { kind: "empty", fields: [] };
2210
+ body3 = { kind: "empty", fields: [] };
1526
2211
  } else {
1527
2212
  let parsed;
1528
2213
  let isJson = true;
@@ -1534,16 +2219,16 @@ function inferShape(res, opts) {
1534
2219
  if (isJson) {
1535
2220
  const acc = /* @__PURE__ */ new Map();
1536
2221
  walk(parsed, "", acc);
1537
- body2 = { kind: "json", rootType: typeOf(parsed), fields: finish(acc, opts.salt) };
2222
+ body3 = { kind: "json", rootType: typeOf(parsed), fields: finish(acc, opts.salt) };
1538
2223
  } else {
1539
- body2 = { kind: "text", fields: [] };
2224
+ body3 = { kind: "text", fields: [] };
1540
2225
  }
1541
2226
  }
1542
2227
  return {
1543
2228
  status: res.status,
1544
2229
  headers: Object.fromEntries(Object.entries(headers).sort(([a], [b]) => cmp3(a, b))),
1545
2230
  headerNames: [...new Set(headerNames)].sort(),
1546
- body: body2
2231
+ body: body3
1547
2232
  };
1548
2233
  }
1549
2234
  function walk(value2, path13, acc) {
@@ -1939,8 +2624,181 @@ function replay(recording, store, secret) {
1939
2624
  return { recording, observations, after, before, ...before ? { diff: diffShapes(before.shape, after.shape) } : {} };
1940
2625
  }
1941
2626
 
1942
- // packages/repair/src/recipes/status-code-accept-set.ts
2627
+ // packages/repair/src/recipes/apply-fix.ts
1943
2628
  import { Node as Node3, SyntaxKind as SyntaxKind3 } from "ts-morph";
2629
+ var applyFix = {
2630
+ id: "apply-fix",
2631
+ changeClass: "version-sunset",
2632
+ description: "Apply the advisory's fix: the new path, query value or header, and the renamed fields and enum values",
2633
+ edits({ sourceFile, call, advisory }) {
2634
+ const fix2 = advisory.fix;
2635
+ if (!fix2) return [];
2636
+ const where = (n) => `${sourceFile.getFilePath().slice(1)}:${n.getStartLineNumber()}`;
2637
+ const edits = [...requestEdits(call, sourceFile, fix2, where), ...headerEdits(call, fix2, where)];
2638
+ const scope = enclosingFunction(call) ?? sourceFile;
2639
+ edits.push(...fieldEdits(scope, fix2, where, edits), ...enumEdits(scope, fix2, where));
2640
+ return edits;
2641
+ }
2642
+ };
2643
+ var TEXT_KINDS = [SyntaxKind3.StringLiteral, SyntaxKind3.NoSubstitutionTemplateLiteral, SyntaxKind3.TemplateHead, SyntaxKind3.TemplateMiddle, SyntaxKind3.TemplateTail];
2644
+ function requestEdits(call, sf, fix2, where) {
2645
+ if (!fix2.path && !fix2.query) return [];
2646
+ const edits = [];
2647
+ for (const node of urlTextNodes(call, sf)) {
2648
+ const before = node.getText();
2649
+ let after = before;
2650
+ if (fix2.path) {
2651
+ after = after.split(fix2.path.from).join(fix2.path.to);
2652
+ const from = fix2.path.from.replace(/\/$/, "");
2653
+ const to = fix2.path.to.replace(/\/$/, "");
2654
+ if (from !== fix2.path.from) after = after.replace(new RegExp(`${escapeRe(from)}(?=["'\`]|\\$\\{)`, "g"), to);
2655
+ }
2656
+ if (fix2.query) {
2657
+ const re = new RegExp(`([?&])${escapeRe(fix2.query.name)}=([^&#\`'"$]+)`, "g");
2658
+ after = after.replace(re, (_m, sep) => `${sep}${fix2.query.name}=${fix2.query.value}`);
2659
+ }
2660
+ if (after !== before) edits.push({ start: node.getStart(), end: node.getEnd(), text: after, note: `${where(node)} \`${before}\` \u2192 \`${after}\`` });
2661
+ }
2662
+ if (fix2.query) {
2663
+ for (const prop of [...objectProperties(call, "params"), ...objectProperties(call, "searchParams")]) {
2664
+ if (propName2(prop) !== fix2.query.name || !Node3.isPropertyAssignment(prop)) continue;
2665
+ const init = prop.getInitializer();
2666
+ if (!init || !(Node3.isNumericLiteral(init) || Node3.isStringLiteral(init))) continue;
2667
+ const text = /^\d+(\.\d+)?$/.test(fix2.query.value) && Node3.isNumericLiteral(init) ? fix2.query.value : JSON.stringify(fix2.query.value);
2668
+ if (init.getText() !== text) edits.push({ start: init.getStart(), end: init.getEnd(), text, note: `${where(init)} ${fix2.query.name}: \`${init.getText()}\` \u2192 \`${text}\`` });
2669
+ }
2670
+ }
2671
+ return edits;
2672
+ }
2673
+ function headerEdits(call, fix2, where) {
2674
+ if (!fix2.header) return [];
2675
+ const edits = [];
2676
+ const text = JSON.stringify(fix2.header.value);
2677
+ for (const prop of objectProperties(call, "headers")) {
2678
+ if (propName2(prop)?.toLowerCase() !== fix2.header.name.toLowerCase() || !Node3.isPropertyAssignment(prop)) continue;
2679
+ const init = prop.getInitializer();
2680
+ if (!init || !(Node3.isStringLiteral(init) || Node3.isNoSubstitutionTemplateLiteral(init)) || init.getLiteralText() === fix2.header.value) continue;
2681
+ edits.push({ start: init.getStart(), end: init.getEnd(), text, note: `${where(init)} ${fix2.header.name}: \`${init.getText()}\` \u2192 \`${text}\`` });
2682
+ }
2683
+ return edits;
2684
+ }
2685
+ function fieldEdits(scope, fix2, where, taken) {
2686
+ const edits = [];
2687
+ for (const { from, to } of fix2.fields ?? []) {
2688
+ const oldName = leaf(from);
2689
+ const newName = leaf(to);
2690
+ if (oldName === newName) continue;
2691
+ for (const access of scope.getDescendantsOfKind(SyntaxKind3.PropertyAccessExpression)) {
2692
+ if (access.getName() !== oldName || isWrite(access) || overlaps(access, [...taken, ...edits])) continue;
2693
+ const target = access.getExpression().getText();
2694
+ const dot = access.hasQuestionDotToken() ? "?." : ".";
2695
+ const text = `(${target}${dot}${newName} ?? ${target}${dot}${oldName})`;
2696
+ edits.push({ start: access.getStart(), end: access.getEnd(), text, note: `${where(access)} \`${access.getText()}\` \u2192 \`${text}\`` });
2697
+ }
2698
+ }
2699
+ return edits;
2700
+ }
2701
+ function enumEdits(scope, fix2, where) {
2702
+ const edits = [];
2703
+ for (const { field, values } of fix2.enums ?? []) {
2704
+ const name = leaf(field);
2705
+ const renamed = new Map(values.map((v) => [v.from, v.to]));
2706
+ const isField = (n) => Node3.isPropertyAccessExpression(n) && n.getName() === name || Node3.isIdentifier(n) && n.getText() === name;
2707
+ for (const bin of scope.getDescendantsOfKind(SyntaxKind3.BinaryExpression)) {
2708
+ const op = bin.getOperatorToken().getKind();
2709
+ const negated = op === SyntaxKind3.ExclamationEqualsEqualsToken || op === SyntaxKind3.ExclamationEqualsToken;
2710
+ if (!negated && op !== SyntaxKind3.EqualsEqualsEqualsToken && op !== SyntaxKind3.EqualsEqualsToken) continue;
2711
+ const [lit, other] = Node3.isStringLiteral(bin.getRight()) ? [bin.getRight(), bin.getLeft()] : [bin.getLeft(), bin.getRight()];
2712
+ if (!Node3.isStringLiteral(lit) || !isField(other)) continue;
2713
+ const to = renamed.get(lit.getLiteralText());
2714
+ if (to === void 0) continue;
2715
+ const text = `${negated ? "!" : ""}${JSON.stringify([lit.getLiteralText(), to])}.includes(${other.getText()})`;
2716
+ edits.push({ start: bin.getStart(), end: bin.getEnd(), text, note: `${where(bin)} \`${bin.getText()}\` \u2192 \`${text}\`` });
2717
+ }
2718
+ for (const sw of scope.getDescendantsOfKind(SyntaxKind3.SwitchStatement)) {
2719
+ if (!isField(sw.getExpression())) continue;
2720
+ const cases = sw.getClauses().filter(Node3.isCaseClause);
2721
+ const present = new Set(cases.map((c) => c.getExpression()).filter(Node3.isStringLiteral).map((l) => l.getLiteralText()));
2722
+ for (const c of cases) {
2723
+ const e = c.getExpression();
2724
+ const to = Node3.isStringLiteral(e) ? renamed.get(e.getLiteralText()) : void 0;
2725
+ if (to === void 0 || present.has(to)) continue;
2726
+ present.add(to);
2727
+ const indent = " ".repeat(c.getSourceFile().getLineAndColumnAtPos(c.getStart()).column - 1);
2728
+ edits.push({ start: c.getStart(), end: c.getStart(), text: `case ${JSON.stringify(to)}:
2729
+ ${indent}`, note: `${where(c)} add case ${JSON.stringify(to)}` });
2730
+ }
2731
+ }
2732
+ for (const inc of scope.getDescendantsOfKind(SyntaxKind3.CallExpression)) {
2733
+ const callee = inc.getExpression();
2734
+ if (!Node3.isPropertyAccessExpression(callee) || callee.getName() !== "includes" || !isField(inc.getArguments()[0] ?? inc)) continue;
2735
+ const arr = callee.getExpression();
2736
+ if (!Node3.isArrayLiteralExpression(arr)) continue;
2737
+ const items = arr.getElements().filter(Node3.isStringLiteral).map((l) => l.getLiteralText());
2738
+ const add = items.map((v) => renamed.get(v)).filter((v) => v !== void 0 && !items.includes(v));
2739
+ if (!add.length) continue;
2740
+ const text = JSON.stringify([...items, ...new Set(add)]);
2741
+ edits.push({ start: arr.getStart(), end: arr.getEnd(), text, note: `${where(arr)} \`${arr.getText()}\` \u2192 \`${text}\`` });
2742
+ }
2743
+ }
2744
+ return edits;
2745
+ }
2746
+ function urlTextNodes(call, sf) {
2747
+ const out = /* @__PURE__ */ new Set();
2748
+ const seen = /* @__PURE__ */ new Set();
2749
+ const visit = (n, depth) => {
2750
+ if (!n || depth > 4 || seen.has(n)) return;
2751
+ seen.add(n);
2752
+ if (TEXT_KINDS.includes(n.getKind())) out.add(n);
2753
+ if (Node3.isIdentifier(n)) {
2754
+ const decl = sf.getVariableDeclaration(n.getText());
2755
+ if (decl?.getVariableStatement()?.getDeclarationKind() === "const") visit(decl.getInitializer(), depth + 1);
2756
+ return;
2757
+ }
2758
+ n.forEachChild((c) => visit(c, depth));
2759
+ };
2760
+ const [first] = call.getArguments();
2761
+ if (first && Node3.isObjectLiteralExpression(first)) {
2762
+ const url = first.getProperty("url");
2763
+ if (url && Node3.isPropertyAssignment(url)) visit(url.getInitializer(), 0);
2764
+ } else visit(first, 0);
2765
+ for (const prop of sf.getDescendantsOfKind(SyntaxKind3.PropertyAssignment)) if (propName2(prop) === "baseURL") visit(prop.getInitializer(), 0);
2766
+ return [...out];
2767
+ }
2768
+ function objectProperties(call, name) {
2769
+ const out = [];
2770
+ for (const arg of call.getArguments()) {
2771
+ if (!Node3.isObjectLiteralExpression(arg)) continue;
2772
+ const prop = arg.getProperties().find((p) => propName2(p) === name);
2773
+ const value2 = prop && Node3.isPropertyAssignment(prop) ? prop.getInitializer() : void 0;
2774
+ if (value2 && Node3.isObjectLiteralExpression(value2)) out.push(...value2.getProperties());
2775
+ }
2776
+ return out;
2777
+ }
2778
+ function propName2(p) {
2779
+ if (!(Node3.isPropertyAssignment(p) || Node3.isShorthandPropertyAssignment(p))) return void 0;
2780
+ const n = p.getNameNode();
2781
+ return Node3.isStringLiteral(n) || Node3.isNoSubstitutionTemplateLiteral(n) ? n.getLiteralText() : n.getText();
2782
+ }
2783
+ function enclosingFunction(call) {
2784
+ return call.getFirstAncestor((n) => Node3.isFunctionDeclaration(n) || Node3.isArrowFunction(n) || Node3.isFunctionExpression(n) || Node3.isMethodDeclaration(n));
2785
+ }
2786
+ function isWrite(access) {
2787
+ const p = access.getParent();
2788
+ return Node3.isBinaryExpression(p) && p.getLeft() === access && p.getOperatorToken().getKind() === SyntaxKind3.EqualsToken;
2789
+ }
2790
+ function overlaps(n, edits) {
2791
+ return edits.some((e) => n.getStart() < e.end && e.start < n.getEnd());
2792
+ }
2793
+ function leaf(path13) {
2794
+ return path13.split(".").pop().replace(/\[\]/g, "");
2795
+ }
2796
+ function escapeRe(s) {
2797
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2798
+ }
2799
+
2800
+ // packages/repair/src/recipes/status-code-accept-set.ts
2801
+ import { Node as Node4, SyntaxKind as SyntaxKind4 } from "ts-morph";
1944
2802
  var STATUS_PROPS = /* @__PURE__ */ new Set(["status", "statusCode"]);
1945
2803
  var statusCodeAcceptSet = {
1946
2804
  id: "status-code-accept-set",
@@ -1955,23 +2813,23 @@ var statusCodeAcceptSet = {
1955
2813
  const where = (n) => `${sourceFile.getFilePath().slice(1)}:${n.getStartLineNumber()}`;
1956
2814
  for (const status of statusExpressions(call)) {
1957
2815
  let cur = status;
1958
- while (Node3.isParenthesizedExpression(cur.getParent())) cur = cur.getParent();
2816
+ while (Node4.isParenthesizedExpression(cur.getParent())) cur = cur.getParent();
1959
2817
  const parent = cur.getParent();
1960
- if (Node3.isBinaryExpression(parent)) {
2818
+ if (Node4.isBinaryExpression(parent)) {
1961
2819
  const op = parent.getOperatorToken().getKind();
1962
2820
  const other = parent.getLeft() === cur ? parent.getRight() : parent.getLeft();
1963
- if (!Node3.isNumericLiteral(other) || !oldCodes.includes(Number(other.getLiteralValue()))) continue;
1964
- const negated = op === SyntaxKind3.ExclamationEqualsEqualsToken || op === SyntaxKind3.ExclamationEqualsToken;
1965
- const equal = op === SyntaxKind3.EqualsEqualsEqualsToken || op === SyntaxKind3.EqualsEqualsToken;
2821
+ if (!Node4.isNumericLiteral(other) || !oldCodes.includes(Number(other.getLiteralValue()))) continue;
2822
+ const negated = op === SyntaxKind4.ExclamationEqualsEqualsToken || op === SyntaxKind4.ExclamationEqualsToken;
2823
+ const equal = op === SyntaxKind4.EqualsEqualsEqualsToken || op === SyntaxKind4.EqualsEqualsToken;
1966
2824
  if (!negated && !equal) continue;
1967
2825
  const text = `${negated ? "!" : ""}${list}.includes(${status.getText()})`;
1968
2826
  edits.push({ start: parent.getStart(), end: parent.getEnd(), text, note: `${where(parent)} \`${parent.getText()}\` \u2192 \`${text}\`` });
1969
- } else if (Node3.isSwitchStatement(parent) && parent.getExpression() === cur) {
1970
- const cases = parent.getClauses().filter(Node3.isCaseClause);
1971
- const present = new Set(cases.map((c) => c.getExpression()).filter(Node3.isNumericLiteral).map((l) => Number(l.getLiteralValue())));
2827
+ } else if (Node4.isSwitchStatement(parent) && parent.getExpression() === cur) {
2828
+ const cases = parent.getClauses().filter(Node4.isCaseClause);
2829
+ const present = new Set(cases.map((c) => c.getExpression()).filter(Node4.isNumericLiteral).map((l) => Number(l.getLiteralValue())));
1972
2830
  const anchor = cases.find((c) => {
1973
2831
  const e = c.getExpression();
1974
- return Node3.isNumericLiteral(e) && oldCodes.includes(Number(e.getLiteralValue()));
2832
+ return Node4.isNumericLiteral(e) && oldCodes.includes(Number(e.getLiteralValue()));
1975
2833
  });
1976
2834
  const missing = accept.filter((c) => !present.has(c));
1977
2835
  if (!anchor || missing.length === 0) continue;
@@ -1979,11 +2837,11 @@ var statusCodeAcceptSet = {
1979
2837
  const text = missing.map((c) => `case ${c}:
1980
2838
  ${indent}`).join("");
1981
2839
  edits.push({ start: anchor.getStart(), end: anchor.getStart(), text, note: `${where(anchor)} add ${missing.map((c) => `case ${c}`).join(", ")}` });
1982
- } else if (Node3.isCallExpression(parent) && parent.getArguments()[0] === cur) {
2840
+ } else if (Node4.isCallExpression(parent) && parent.getArguments()[0] === cur) {
1983
2841
  const callee = parent.getExpression();
1984
- const arr = Node3.isPropertyAccessExpression(callee) && callee.getName() === "includes" ? callee.getExpression() : void 0;
1985
- if (!arr || !Node3.isArrayLiteralExpression(arr)) continue;
1986
- const values = arr.getElements().filter(Node3.isNumericLiteral).map((l) => Number(l.getLiteralValue()));
2842
+ const arr = Node4.isPropertyAccessExpression(callee) && callee.getName() === "includes" ? callee.getExpression() : void 0;
2843
+ if (!arr || !Node4.isArrayLiteralExpression(arr)) continue;
2844
+ const values = arr.getElements().filter(Node4.isNumericLiteral).map((l) => Number(l.getLiteralValue()));
1987
2845
  if (!values.some((v) => oldCodes.includes(v))) continue;
1988
2846
  const merged = [.../* @__PURE__ */ new Set([...values, ...accept])].sort((a, b) => a - b);
1989
2847
  if (merged.length === values.length) continue;
@@ -1999,52 +2857,52 @@ function statusExpressions(call) {
1999
2857
  let cur = call;
2000
2858
  for (; ; ) {
2001
2859
  const p = cur.getParent();
2002
- if (p && (Node3.isAwaitExpression(p) || Node3.isParenthesizedExpression(p) || Node3.isAsExpression(p) || Node3.isNonNullExpression(p))) cur = p;
2860
+ if (p && (Node4.isAwaitExpression(p) || Node4.isParenthesizedExpression(p) || Node4.isAsExpression(p) || Node4.isNonNullExpression(p))) cur = p;
2003
2861
  else break;
2004
2862
  }
2005
2863
  const parent = cur.getParent();
2006
2864
  const sf = call.getSourceFile();
2007
- if (Node3.isPropertyAccessExpression(parent) && parent.getExpression() === cur && STATUS_PROPS.has(parent.getName())) out.push(parent);
2008
- if (Node3.isPropertyAccessExpression(parent) && parent.getName() === "then") {
2009
- const cb = Node3.isCallExpression(parent.getParent()) ? parent.getParent().getArguments()[0] : void 0;
2010
- const param = cb && (Node3.isArrowFunction(cb) || Node3.isFunctionExpression(cb)) ? cb.getParameters()[0] : void 0;
2865
+ if (Node4.isPropertyAccessExpression(parent) && parent.getExpression() === cur && STATUS_PROPS.has(parent.getName())) out.push(parent);
2866
+ if (Node4.isPropertyAccessExpression(parent) && parent.getName() === "then") {
2867
+ const cb = Node4.isCallExpression(parent.getParent()) ? parent.getParent().getArguments()[0] : void 0;
2868
+ const param = cb && (Node4.isArrowFunction(cb) || Node4.isFunctionExpression(cb)) ? cb.getParameters()[0] : void 0;
2011
2869
  if (param) out.push(...fromBinding(param.getNameNode(), param, sf));
2012
2870
  }
2013
- if (Node3.isVariableDeclaration(parent) && parent.getInitializer() === cur) out.push(...fromBinding(parent.getNameNode(), parent, sf));
2871
+ if (Node4.isVariableDeclaration(parent) && parent.getInitializer() === cur) out.push(...fromBinding(parent.getNameNode(), parent, sf));
2014
2872
  return out;
2015
2873
  }
2016
2874
  function fromBinding(name, decl, sf) {
2017
2875
  const out = [];
2018
- if (Node3.isIdentifier(name)) {
2876
+ if (Node4.isIdentifier(name)) {
2019
2877
  for (const ref of references(name.getText(), decl, sf)) {
2020
2878
  const p = ref.getParent();
2021
- if (Node3.isPropertyAccessExpression(p) && p.getExpression() === ref && STATUS_PROPS.has(p.getName())) out.push(p);
2879
+ if (Node4.isPropertyAccessExpression(p) && p.getExpression() === ref && STATUS_PROPS.has(p.getName())) out.push(p);
2022
2880
  }
2023
- } else if (Node3.isObjectBindingPattern(name)) {
2881
+ } else if (Node4.isObjectBindingPattern(name)) {
2024
2882
  for (const el of name.getElements()) {
2025
2883
  const prop = el.getPropertyNameNode()?.getText() ?? el.getName();
2026
- if (STATUS_PROPS.has(prop) && Node3.isIdentifier(el.getNameNode())) out.push(...references(el.getName(), decl, sf));
2884
+ if (STATUS_PROPS.has(prop) && Node4.isIdentifier(el.getNameNode())) out.push(...references(el.getName(), decl, sf));
2027
2885
  }
2028
2886
  }
2029
2887
  return out;
2030
2888
  }
2031
2889
  function references(name, decl, sf) {
2032
2890
  const scope = decl.getFirstAncestor(
2033
- (n) => Node3.isBlock(n) || Node3.isArrowFunction(n) || Node3.isFunctionExpression(n) || Node3.isFunctionDeclaration(n) || Node3.isMethodDeclaration(n) || Node3.isCaseClause(n)
2891
+ (n) => Node4.isBlock(n) || Node4.isArrowFunction(n) || Node4.isFunctionExpression(n) || Node4.isFunctionDeclaration(n) || Node4.isMethodDeclaration(n) || Node4.isCaseClause(n)
2034
2892
  ) ?? sf;
2035
- return scope.getDescendantsOfKind(SyntaxKind3.Identifier).filter((id) => {
2893
+ return scope.getDescendantsOfKind(SyntaxKind4.Identifier).filter((id) => {
2036
2894
  if (id.getText() !== name || id.getStart() < decl.getEnd()) return false;
2037
2895
  const p = id.getParent();
2038
- if (Node3.isPropertyAccessExpression(p) && p.getNameNode() === id) return false;
2039
- if (Node3.isPropertyAssignment(p) && p.getNameNode() === id) return false;
2040
- return !(Node3.isBindingElement(p) || Node3.isVariableDeclaration(p) || Node3.isParameterDeclaration(p));
2896
+ if (Node4.isPropertyAccessExpression(p) && p.getNameNode() === id) return false;
2897
+ if (Node4.isPropertyAssignment(p) && p.getNameNode() === id) return false;
2898
+ return !(Node4.isBindingElement(p) || Node4.isVariableDeclaration(p) || Node4.isParameterDeclaration(p));
2041
2899
  });
2042
2900
  }
2043
2901
 
2044
2902
  // packages/repair/src/recipe.ts
2045
- import { Project as Project2, ScriptKind as ScriptKind2, SyntaxKind as SyntaxKind4 } from "ts-morph";
2903
+ import { Project as Project2, ScriptKind as ScriptKind2, SyntaxKind as SyntaxKind5 } from "ts-morph";
2046
2904
  function findCall(sf, site) {
2047
- return sf.getDescendantsOfKind(SyntaxKind4.CallExpression).filter((c) => {
2905
+ return sf.getDescendantsOfKind(SyntaxKind5.CallExpression).filter((c) => {
2048
2906
  const pos = sf.getLineAndColumnAtPos(c.getStart());
2049
2907
  return pos.line === site.line && pos.column === site.column;
2050
2908
  }).sort((a, b) => a.getWidth() - b.getWidth())[0];
@@ -2073,7 +2931,7 @@ function runRecipe(recipes, advisory, callSite, text) {
2073
2931
 
2074
2932
  // packages/repair/src/contract.ts
2075
2933
  import path7 from "path";
2076
- import { Node as Node4, Project as Project3, ScriptKind as ScriptKind3, SyntaxKind as SyntaxKind5 } from "ts-morph";
2934
+ import { Node as Node5, Project as Project3, ScriptKind as ScriptKind3, SyntaxKind as SyntaxKind6 } from "ts-morph";
2077
2935
 
2078
2936
  // packages/repair/src/synth.ts
2079
2937
  function synthesizeBody(shape) {
@@ -2132,70 +2990,339 @@ function sampleString(classes) {
2132
2990
  if (classes.includes("token")) return "shapi_value";
2133
2991
  return "shapi text";
2134
2992
  }
2993
+ function bodyFromPaths(paths, values = {}) {
2994
+ const all = [.../* @__PURE__ */ new Set([...paths, ...Object.keys(values)])];
2995
+ const isLeaf = (p) => !all.some((o) => o !== p && (o.startsWith(`${p}.`) || o.startsWith(`${p}[]`)));
2996
+ const shape = {
2997
+ status: 200,
2998
+ headers: {},
2999
+ headerNames: [],
3000
+ body: {
3001
+ kind: "json",
3002
+ rootType: all.some((p) => p.startsWith("[]")) ? "array" : "object",
3003
+ fields: all.map((path13) => ({ path: path13, pathHash: path13, types: isLeaf(path13) ? ["string"] : [], classes: [], occurrences: 1, nullRate: 0 }))
3004
+ }
3005
+ };
3006
+ const body3 = synthesizeBody(shape);
3007
+ for (const [path13, value2] of Object.entries(values)) setPath(body3, tokens(path13), value2);
3008
+ return body3;
3009
+ }
3010
+ function setPath(target, path13, value2) {
3011
+ let cur = target;
3012
+ for (let i = 0; i < path13.length; i++) {
3013
+ const t = path13[i];
3014
+ const last = i === path13.length - 1;
3015
+ if (t === "[]") {
3016
+ const arr = cur;
3017
+ if (last) arr[0] = value2;
3018
+ else cur = arr[0];
3019
+ } else {
3020
+ const obj = cur;
3021
+ if (last) obj[t] = value2;
3022
+ else cur = obj[t];
3023
+ }
3024
+ if (cur === void 0 || cur === null) return;
3025
+ }
3026
+ }
2135
3027
 
2136
3028
  // packages/repair/src/contract.ts
2137
3029
  var CONTRACT_DIR = "test/shapi";
3030
+ var RETIRED_STATUS = 410;
2138
3031
  function generateContractTest(input) {
2139
3032
  const { callSite, advisory, before, after } = input;
2140
- if (callSite.kind !== "http" || callSite.client !== "fetch") {
2141
- return { status: "unsupported", reason: `contract tests support global fetch call sites; this one uses ${callSite.client}` };
2142
- }
3033
+ const fix2 = advisory.fix;
2143
3034
  const project = new Project3({ useInMemoryFileSystem: true, skipFileDependencyResolution: true, compilerOptions: { allowJs: true } });
2144
3035
  const sf = project.createSourceFile(`/${callSite.file}`, input.sourceText, {
2145
3036
  scriptKind: /\.(c|m)?jsx?$/.test(callSite.file) ? ScriptKind3.JS : ScriptKind3.TS
2146
3037
  });
2147
3038
  const call = findCall(sf, callSite);
2148
3039
  if (!call) return { status: "unsupported", reason: `no call at ${callSite.file}:${callSite.line}:${callSite.column}` };
3040
+ const transport = transportFor(callSite, sf);
3041
+ if ("reason" in transport) return { status: "unsupported", reason: transport.reason };
2149
3042
  const fn = exportedFunction(call);
2150
3043
  if (!fn) return { status: "unsupported", reason: "the call site is not inside an exported function" };
2151
- let responses;
3044
+ const file = input.file ?? `${CONTRACT_DIR}/${advisory.id}.test.ts`;
3045
+ const importPath = toImport(path7.posix.relative(path7.posix.dirname(file), callSite.file));
3046
+ const parts = {
3047
+ header: `// Generated by SHAPI for advisory ${advisory.id}: ${advisory.title}
3048
+ // Call site: ${callSite.file}:${callSite.line} (${callSite.method} ${callSite.endpoint}, via ${callSite.client})`,
3049
+ fn: fn.name,
3050
+ importPath,
3051
+ args: fn.params.map((p) => argument(p, sf)).join(", "),
3052
+ advisory,
3053
+ transport
3054
+ };
3055
+ if (fix2 && (fix2.path || fix2.query || fix2.header)) {
3056
+ if (callSite.kind === "sdk") return { status: "unsupported", reason: `the fix changes the request, which ${callSite.client} builds itself` };
3057
+ return { status: "generated", file, content: requestTest({ ...parts, fix: fix2, body: renamePair(callSite, fix2).after }), functionName: fn.name };
3058
+ }
3059
+ if (fix2 && (fix2.fields || fix2.enums)) {
3060
+ return { status: "generated", file, content: sameResultTest({ ...parts, ...renamePair(callSite, fix2) }), functionName: fn.name };
3061
+ }
3062
+ if (!before || !after) return { status: "unsupported", reason: "no before/after responses for this endpoint, and the advisory has no fix to build a test from" };
2152
3063
  try {
2153
- responses = [response(`before (${before.status})`, before), response(`after (${after.status})`, after)].join(",\n");
3064
+ return { status: "generated", file, content: recordedTest({ ...parts, before, after }), functionName: fn.name };
2154
3065
  } catch (err) {
2155
3066
  return { status: "unsupported", reason: err instanceof Error ? err.message : String(err) };
2156
3067
  }
2157
- const file = `${CONTRACT_DIR}/${advisory.id}.test.ts`;
2158
- const importPath = toImport(path7.posix.relative(path7.posix.dirname(file), callSite.file));
2159
- const args = fn.params.map((p) => argument(p, sf)).join(", ");
2160
- const content = `// Generated by SHAPI for advisory ${advisory.id}: ${advisory.title}
2161
- // Call site: ${callSite.file}:${callSite.line} (${callSite.method} ${callSite.endpoint})
2162
- // Calls ${fn.name}() with fetch returning the response the code was written for and the one
2163
- // ${advisory.provider} sends now. It fails while the code accepts only the old response.
2164
- import { afterEach, describe, expect, it, vi } from "vitest";
2165
- import { ${fn.name} } from "${importPath}";
3068
+ }
3069
+ function transportFor(site, sf) {
3070
+ if (site.kind === "http") {
3071
+ if (site.client === "fetch" || site.client === "ky") return { mock: "", stubFetch: true, name: "fetch" };
3072
+ if (site.client === "node-fetch") {
3073
+ return { mock: `vi.mock("node-fetch", () => ({ default: (input: RequestInfo | URL, init?: RequestInit) => globalThis.fetch(input, init) }));
3074
+ `, stubFetch: true, name: "node-fetch" };
3075
+ }
3076
+ if (site.client === "axios") return { mock: AXIOS_FAKE, stubFetch: false, name: "axios" };
3077
+ if (site.client === "got") return { mock: GOT_FAKE, stubFetch: false, name: "got" };
3078
+ return { reason: `contract tests support fetch, node-fetch, ky, axios, got and SDK call sites; this one uses ${site.client}` };
3079
+ }
3080
+ const decl = sf.getImportDeclarations().find((d) => d.getModuleSpecifierValue() === site.client);
3081
+ if (!decl) return { reason: `${site.file} does not import ${site.client} with an import statement` };
3082
+ const names = decl.getNamedImports().map((n) => n.getName());
3083
+ const exports = [`default: client`, ...names.map((n) => `${JSON.stringify(n)}: client`)].join(", ");
3084
+ const url = `https://${site.host ?? "sdk.invalid"}${site.endpoint ?? "/"}`;
3085
+ const envelope = site.provider === "github" ? `{ status: res.status, data: res.body, headers: res.headers }` : `res.body`;
3086
+ return {
3087
+ mock: `vi.mock(${JSON.stringify(site.client)}, () => {
3088
+ const call = async () => {
3089
+ const res = shapi.respond({ method: ${JSON.stringify(site.method ?? "GET")}, url: ${JSON.stringify(url)}, headers: {} });
3090
+ if (res.status >= 400) throw Object.assign(new Error(\`${site.client} request failed with \${res.status}\`), { status: res.status, statusCode: res.status, response: { status: res.status, data: res.body } });
3091
+ return ${envelope};
3092
+ };
3093
+ const client: any = new Proxy(function () {}, {
3094
+ get: (_t, prop) => (prop === "then" || typeof prop === "symbol" ? undefined : client),
3095
+ apply: () => call(),
3096
+ construct: () => client,
3097
+ });
3098
+ return { ${exports} };
3099
+ });
3100
+ `,
3101
+ stubFetch: false,
3102
+ name: `a fake ${site.client} client`
3103
+ };
3104
+ }
3105
+ function preamble(p) {
3106
+ return `import { afterEach, describe, expect, it, vi } from "vitest";
2166
3107
 
3108
+ type ShapiRequest = { method: string; url: string; headers: Record<string, string> };
3109
+ type ShapiResponse = { status: number; body: unknown; headers: Record<string, string> };
3110
+ const shapi = vi.hoisted(() => ({
3111
+ respond: (_req: { method: string; url: string; headers: Record<string, string> }): { status: number; body: unknown; headers: Record<string, string> } => ({ status: 200, body: null, headers: {} }),
3112
+ lower: (h: unknown): Record<string, string> =>
3113
+ Object.fromEntries(Object.entries((h ?? {}) as Record<string, unknown>).filter(([, v]) => v !== undefined).map(([k, v]) => [k.toLowerCase(), String(v)])),
3114
+ }));
3115
+ ${p.transport.mock}
3116
+ import { ${p.fn} } from "${p.importPath}";
3117
+ ${p.transport.stubFetch ? `
3118
+ const shapiFetch = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
3119
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
3120
+ const headers = Object.fromEntries(new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)).entries());
3121
+ const res = shapi.respond({ method: (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase(), url, headers });
3122
+ return new Response(res.body === null ? null : JSON.stringify(res.body), { status: res.status, headers: res.headers });
3123
+ });
3124
+ ` : ""}
3125
+ afterEach(() => {
3126
+ vi.unstubAllGlobals();
3127
+ });
3128
+
3129
+ async function run(respond: (req: ShapiRequest) => ShapiResponse): Promise<unknown> {
3130
+ shapi.respond = respond;${p.transport.stubFetch ? `
3131
+ vi.stubGlobal("fetch", shapiFetch);` : ""}
3132
+ return ${p.fn}(${p.args});
3133
+ }
3134
+ `;
3135
+ }
3136
+ var JSON_HEADERS = `{ "content-type": "application/json" }`;
3137
+ function requestTest(p) {
3138
+ const checks = [];
3139
+ if (p.fix.path) checks.push(`url.href.includes(${JSON.stringify(p.fix.path.from)})`);
3140
+ if (p.fix.query) {
3141
+ const name = JSON.stringify(p.fix.query.name);
3142
+ const below = p.advisory.trigger.query?.find((q) => q.name === p.fix.query.name)?.below;
3143
+ checks.push(
3144
+ below === void 0 ? `(url.searchParams.has(${name}) && url.searchParams.get(${name}) !== ${JSON.stringify(p.fix.query.value)})` : `(url.searchParams.has(${name}) && Number(url.searchParams.get(${name})) < ${below})`
3145
+ );
3146
+ }
3147
+ if (p.fix.header) {
3148
+ const name = JSON.stringify(p.fix.header.name.toLowerCase());
3149
+ checks.push(`(${name} in req.headers && req.headers[${name}] !== ${JSON.stringify(p.fix.header.value)})`);
3150
+ }
3151
+ return `${p.header}
3152
+ // The provider no longer accepts the old request: ${p.transport.name} is replaced by a stub that answers it
3153
+ // with ${RETIRED_STATUS}, and answers anything else with a body holding the fields the code reads. The test
3154
+ // fails until ${p.fn}() sends the new request (${describeFix(p.fix)}) and still works.
3155
+ ${preamble(p)}
3156
+ const body = ${JSON.stringify(p.body)};
3157
+
3158
+ function retired(req: ShapiRequest): boolean {
3159
+ const url = new URL(req.url, "https://shapi.invalid");
3160
+ return ${checks.join(" || ")};
3161
+ }
3162
+
3163
+ describe("SHAPI contract: ${p.advisory.id}", () => {
3164
+ it("${p.fn} sends the new request and handles the response", async () => {
3165
+ const sent: boolean[] = [];
3166
+ await run((req) => {
3167
+ const old = retired(req);
3168
+ sent.push(old);
3169
+ return old ? { status: ${RETIRED_STATUS}, body: { error: "retired request (SHAPI contract test)" }, headers: ${JSON_HEADERS} } : { status: 200, body, headers: ${JSON_HEADERS} };
3170
+ });
3171
+ expect(sent.length).toBeGreaterThan(0);
3172
+ expect(sent).not.toContain(true);
3173
+ });
3174
+ });
3175
+ `;
3176
+ }
3177
+ function sameResultTest(p) {
3178
+ return `${p.header}
3179
+ // The provider renamed fields or enum values. This test calls ${p.fn}() once with the old response and
3180
+ // once with the new one (${p.transport.name} replaced by a stub), and expects the same result: it fails
3181
+ // while the code only reads the old names.
3182
+ ${preamble(p)}
2167
3183
  const responses = [
2168
- ${responses},
3184
+ { name: "old", body: ${JSON.stringify(p.before)} },
3185
+ { name: "new", body: ${JSON.stringify(p.after)} },
2169
3186
  ];
2170
3187
 
2171
- describe("SHAPI contract: ${advisory.id}", () => {
2172
- afterEach(() => {
2173
- vi.unstubAllGlobals();
3188
+ describe("SHAPI contract: ${p.advisory.id}", () => {
3189
+ it("${p.fn} returns the same result for the old and the new response", async () => {
3190
+ const results: unknown[] = [];
3191
+ for (const r of responses) results.push(await run(() => ({ status: 200, body: r.body, headers: ${JSON_HEADERS} })));
3192
+ expect(results[1]).toEqual(results[0]);
2174
3193
  });
3194
+ });
3195
+ `;
3196
+ }
3197
+ function recordedTest(p) {
3198
+ const responses = [response(`before (${p.before.status})`, p.before), response(`after (${p.after.status})`, p.after)].join(",\n");
3199
+ return `${p.header}
3200
+ // Calls ${p.fn}() with ${p.transport.name} returning the response the code was written for and the one
3201
+ // ${p.advisory.provider} sends now. It fails while the code accepts only the old response.
3202
+ ${preamble(p)}
3203
+ const responses = [
3204
+ ${responses},
3205
+ ];
2175
3206
 
3207
+ describe("SHAPI contract: ${p.advisory.id}", () => {
2176
3208
  for (const r of responses) {
2177
- it(\`${fn.name} handles the \${r.name} response\`, async () => {
2178
- const fetchMock = vi.fn(async () => new Response(r.body === null ? null : JSON.stringify(r.body), { status: r.status, headers: r.headers }));
2179
- vi.stubGlobal("fetch", fetchMock);
2180
- await ${fn.name}(${args});
2181
- expect(fetchMock).toHaveBeenCalled();
3209
+ it(\`${p.fn} handles the \${r.name} response\`, async () => {
3210
+ const seen: ShapiRequest[] = [];
3211
+ await run((req) => (seen.push(req), { status: r.status, body: r.body, headers: r.headers }));
3212
+ expect(seen.length).toBeGreaterThan(0);
2182
3213
  });
2183
3214
  }
2184
3215
  });
2185
3216
  `;
2186
- return { status: "generated", file, content, functionName: fn.name };
2187
3217
  }
3218
+ function renamePair(site, fix2) {
3219
+ const paths = site.surface.fields;
3220
+ const rename = (p) => {
3221
+ for (const { from, to } of fix2.fields ?? []) {
3222
+ if (!/[.[]/.test(from)) {
3223
+ const segs = p.split(".");
3224
+ const i = segs.findIndex((seg) => seg.replace(/\[\]/g, "") === from);
3225
+ if (i >= 0) return [...segs.slice(0, i), segs[i].replace(from, to), ...segs.slice(i + 1)].join(".");
3226
+ } else if (p === from || p.startsWith(`${from}.`) || p.startsWith(`${from}[]`)) return to + p.slice(from.length);
3227
+ }
3228
+ return p;
3229
+ };
3230
+ const oldValues = {};
3231
+ const newValues = {};
3232
+ for (const { field, values } of fix2.enums ?? []) {
3233
+ const compared = site.surface.enums.find((e) => e.field === field)?.values ?? [];
3234
+ const pick = values.find((v) => compared.includes(v.from)) ?? values[0];
3235
+ oldValues[field] = pick.from;
3236
+ newValues[rename(field)] = pick.to;
3237
+ }
3238
+ return { before: bodyFromPaths(paths, oldValues), after: bodyFromPaths(paths.map(rename), newValues) };
3239
+ }
3240
+ function describeFix(fix2) {
3241
+ const parts = [];
3242
+ if (fix2.path) parts.push(`${fix2.path.to} instead of ${fix2.path.from}`);
3243
+ if (fix2.query) parts.push(`${fix2.query.name}=${fix2.query.value}`);
3244
+ if (fix2.header) parts.push(`${fix2.header.name}: ${fix2.header.value}`);
3245
+ return parts.join(", ");
3246
+ }
3247
+ var AXIOS_FAKE = `vi.mock("axios", () => {
3248
+ class AxiosError extends Error {
3249
+ isAxiosError = true;
3250
+ constructor(message: string, readonly response?: unknown, readonly status?: number) {
3251
+ super(message);
3252
+ }
3253
+ }
3254
+ const build = (defaults: any = {}): any => {
3255
+ const send = async (config: any) => {
3256
+ const c = { ...defaults, ...config, headers: { ...defaults.headers, ...config.headers }, params: { ...defaults.params, ...config.params } };
3257
+ let url = String(c.url ?? "");
3258
+ if (c.baseURL && !/^[a-z]+:\\/\\//i.test(url)) url = String(c.baseURL).replace(/\\/+$/, "") + "/" + url.replace(/^\\/+/, "");
3259
+ const qs = new URLSearchParams(Object.entries(c.params).filter(([, v]) => v !== undefined && v !== null).map(([k, v]) => [k, String(v)])).toString();
3260
+ if (qs) url += (url.includes("?") ? "&" : "?") + qs;
3261
+ const res = shapi.respond({ method: String(c.method ?? "get").toUpperCase(), url, headers: shapi.lower(c.headers) });
3262
+ const response = { status: res.status, statusText: "", headers: res.headers, data: res.body, config: c };
3263
+ if (res.status < 200 || res.status >= 300) throw new AxiosError(\`Request failed with status code \${res.status}\`, response, res.status);
3264
+ return response;
3265
+ };
3266
+ const instance: any = (a: any, b?: any) => send(typeof a === "string" ? { ...b, url: a } : a);
3267
+ instance.request = (c: any) => send(c);
3268
+ for (const m of ["get", "delete", "head", "options"]) instance[m] = (url: string, c: any = {}) => send({ ...c, url, method: m });
3269
+ for (const m of ["post", "put", "patch"]) instance[m] = (url: string, data: unknown, c: any = {}) => send({ ...c, url, data, method: m });
3270
+ instance.create = (d: any = {}) => build({ ...defaults, ...d, headers: { ...defaults.headers, ...d.headers } });
3271
+ instance.defaults = defaults;
3272
+ instance.interceptors = { request: { use: () => 0, eject: () => {} }, response: { use: () => 0, eject: () => {} } };
3273
+ instance.isAxiosError = (e: any) => Boolean(e?.isAxiosError);
3274
+ return instance;
3275
+ };
3276
+ const axios = build();
3277
+ return { default: axios, create: axios.create, isAxiosError: axios.isAxiosError, AxiosError };
3278
+ });
3279
+ `;
3280
+ var GOT_FAKE = `vi.mock("got", () => {
3281
+ class HTTPError extends Error {
3282
+ constructor(readonly response: any) {
3283
+ super(\`Response code \${response.statusCode}\`);
3284
+ }
3285
+ }
3286
+ const build = (defaults: any = {}): any => {
3287
+ const call = (input: any, options: any = {}) => {
3288
+ const fromObject = typeof input === "object" && !(input instanceof URL);
3289
+ const o = { ...defaults, ...(fromObject ? input : options), headers: { ...defaults.headers, ...(fromObject ? input.headers : options.headers) } };
3290
+ let url = fromObject ? String(o.url ?? "") : String(input);
3291
+ if (o.prefixUrl) url = String(o.prefixUrl).replace(/\\/+$/, "") + "/" + url.replace(/^\\/+/, "");
3292
+ const sp = o.searchParams;
3293
+ const qs = sp ? new URLSearchParams(typeof sp === "string" || sp instanceof URLSearchParams ? sp : Object.entries(sp).map(([k, v]) => [k, String(v)])).toString() : "";
3294
+ if (qs) url += (url.includes("?") ? "&" : "?") + qs;
3295
+ const res = shapi.respond({ method: String(o.method ?? "GET").toUpperCase(), url, headers: shapi.lower(o.headers) });
3296
+ const text = res.body === null ? "" : JSON.stringify(res.body);
3297
+ const response = { statusCode: res.status, headers: res.headers, body: o.responseType === "json" ? res.body : text, url };
3298
+ const failed = (o.throwHttpErrors ?? true) && res.status >= 400;
3299
+ const settle = <T,>(value: T): Promise<T> => (failed ? Promise.reject(new HTTPError(response)) : Promise.resolve(value));
3300
+ const promise: any = settle(response);
3301
+ promise.catch(() => {});
3302
+ promise.json = () => settle(res.body);
3303
+ promise.text = () => settle(text);
3304
+ promise.buffer = () => settle(Buffer.from(text));
3305
+ return promise;
3306
+ };
3307
+ const got: any = (input: any, options?: any) => call(input, options);
3308
+ for (const m of ["get", "post", "put", "patch", "delete", "head"]) got[m] = (input: any, options: any = {}) => call(input, { ...options, method: m.toUpperCase() });
3309
+ got.extend = (...opts: any[]) => build(Object.assign({}, defaults, ...opts));
3310
+ return got;
3311
+ };
3312
+ return { default: build(), HTTPError };
3313
+ });
3314
+ `;
2188
3315
  function response(name, shape) {
2189
- const body2 = synthesizeBody(shape);
3316
+ const body3 = synthesizeBody(shape);
2190
3317
  const headers = shape.headers["content-type"] ? { "content-type": shape.headers["content-type"] } : {};
2191
- return ` { name: ${JSON.stringify(name)}, status: ${shape.status}, headers: ${JSON.stringify(headers)}, body: ${JSON.stringify(body2)} }`;
3318
+ return ` { name: ${JSON.stringify(name)}, status: ${shape.status}, headers: ${JSON.stringify(headers)}, body: ${JSON.stringify(body3)} }`;
2192
3319
  }
2193
3320
  function exportedFunction(call) {
2194
3321
  for (const anc of call.getAncestors()) {
2195
- if (Node4.isFunctionDeclaration(anc) && anc.isExported() && anc.getName()) return { name: anc.getName(), params: anc.getParameters() };
2196
- if (Node4.isArrowFunction(anc) || Node4.isFunctionExpression(anc)) {
3322
+ if (Node5.isFunctionDeclaration(anc) && anc.isExported() && anc.getName()) return { name: anc.getName(), params: anc.getParameters() };
3323
+ if (Node5.isArrowFunction(anc) || Node5.isFunctionExpression(anc)) {
2197
3324
  const decl = anc.getParent();
2198
- if (Node4.isVariableDeclaration(decl) && decl.getVariableStatement()?.isExported()) return { name: decl.getName(), params: anc.getParameters() };
3325
+ if (Node5.isVariableDeclaration(decl) && decl.getVariableStatement()?.isExported()) return { name: decl.getName(), params: anc.getParameters() };
2199
3326
  }
2200
3327
  }
2201
3328
  return void 0;
@@ -2207,22 +3334,22 @@ function argument(p, sf) {
2207
3334
  function valueFor(type, name, sf, depth) {
2208
3335
  if (depth > 4) return "undefined as never";
2209
3336
  switch (type.getKind()) {
2210
- case SyntaxKind5.StringKeyword:
3337
+ case SyntaxKind6.StringKeyword:
2211
3338
  return JSON.stringify(`shapi-${name}`);
2212
- case SyntaxKind5.NumberKeyword:
3339
+ case SyntaxKind6.NumberKeyword:
2213
3340
  return "1";
2214
- case SyntaxKind5.BooleanKeyword:
3341
+ case SyntaxKind6.BooleanKeyword:
2215
3342
  return "true";
2216
3343
  }
2217
- if (Node4.isTypeLiteral(type)) return objectFrom(type.getProperties(), sf, depth);
2218
- if (Node4.isTypeReference(type)) {
3344
+ if (Node5.isTypeLiteral(type)) return objectFrom(type.getProperties(), sf, depth);
3345
+ if (Node5.isTypeReference(type)) {
2219
3346
  const ref = type.getTypeName().getText();
2220
3347
  const iface = sf.getInterface(ref);
2221
3348
  if (iface) return objectFrom(iface.getProperties(), sf, depth);
2222
3349
  const alias = sf.getTypeAlias(ref)?.getTypeNode();
2223
3350
  if (alias) return valueFor(alias, name, sf, depth + 1);
2224
3351
  }
2225
- if (Node4.isArrayTypeNode(type)) return "[]";
3352
+ if (Node5.isArrayTypeNode(type)) return "[]";
2226
3353
  return "undefined as never";
2227
3354
  }
2228
3355
  function objectFrom(props, sf, depth) {
@@ -2242,13 +3369,13 @@ import path10 from "path";
2242
3369
 
2243
3370
  // packages/repair/src/runner.ts
2244
3371
  import { spawnSync } from "child_process";
2245
- import { existsSync } from "fs";
2246
- import { createRequire } from "module";
3372
+ import { existsSync as existsSync2 } from "fs";
3373
+ import { createRequire as createRequire2 } from "module";
2247
3374
  import path8 from "path";
2248
3375
  function vitestCli(dir) {
2249
3376
  const local = path8.join(dir, "node_modules", "vitest", "vitest.mjs");
2250
- if (existsSync(local)) return local;
2251
- const require2 = createRequire(import.meta.url);
3377
+ if (existsSync2(local)) return local;
3378
+ const require2 = createRequire2(import.meta.url);
2252
3379
  return path8.join(path8.dirname(require2.resolve("vitest/package.json")), "vitest.mjs");
2253
3380
  }
2254
3381
  function runTestFile(dir, file, timeoutMs = 12e4) {
@@ -2272,7 +3399,7 @@ function runFailure(output) {
2272
3399
  }
2273
3400
 
2274
3401
  // packages/repair/src/workspace.ts
2275
- import { cpSync, existsSync as existsSync2, mkdirSync as mkdirSync4, mkdtempSync, readFileSync as readFileSync5, symlinkSync, writeFileSync as writeFileSync3 } from "fs";
3402
+ import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync4, mkdtempSync, readFileSync as readFileSync5, symlinkSync, writeFileSync as writeFileSync3 } from "fs";
2276
3403
  import os from "os";
2277
3404
  import path9 from "path";
2278
3405
  var COPY_SKIP = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "coverage", ".shapi"]);
@@ -2283,12 +3410,12 @@ function localWorkspace(repoDir) {
2283
3410
  filter: (src) => !COPY_SKIP.has(path9.basename(src)) || path9.resolve(src) === path9.resolve(repoDir)
2284
3411
  });
2285
3412
  const modules = path9.join(repoDir, "node_modules");
2286
- if (existsSync2(modules)) symlinkSync(modules, path9.join(dir, "node_modules"), "junction");
3413
+ if (existsSync3(modules)) symlinkSync(modules, path9.join(dir, "node_modules"), "junction");
2287
3414
  return {
2288
3415
  location: dir,
2289
3416
  async read(file) {
2290
3417
  const abs = path9.join(dir, file);
2291
- return existsSync2(abs) ? readFileSync5(abs, "utf8") : void 0;
3418
+ return existsSync3(abs) ? readFileSync5(abs, "utf8") : void 0;
2292
3419
  },
2293
3420
  async write(file, content) {
2294
3421
  const abs = path9.join(dir, file);
@@ -2306,10 +3433,13 @@ var PROTECTED = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "covera
2306
3433
  async function repairCallSite(input) {
2307
3434
  const { advisory, callSite } = input;
2308
3435
  if (!input.workspace && !input.repoDir) throw new Error("repairCallSite needs a workspace or a repoDir");
3436
+ if (!/\.(c|m)?[jt]sx?$/.test(callSite.file)) {
3437
+ return { status: "unsupported", changes: [], attempts: [], reason: `fixes support TypeScript and JavaScript; ${callSite.file} is scanned only` };
3438
+ }
2309
3439
  const ws = input.workspace ?? localWorkspace(input.repoDir);
2310
3440
  const original = await ws.read(callSite.file);
2311
3441
  if (original === void 0) return { status: "unsupported", sandbox: ws.location, changes: [], attempts: [], reason: `${callSite.file} is not in the repository` };
2312
- const test = generateContractTest({ callSite, advisory, before: input.before, after: input.after, sourceText: original });
3442
+ const test = generateContractTest({ callSite, advisory, before: input.before, after: input.after, sourceText: original, file: input.testFile });
2313
3443
  if (test.status !== "generated") return { status: "unsupported", changes: [], attempts: [], reason: test.reason };
2314
3444
  await ws.write(test.file, test.content);
2315
3445
  const testChange = { file: test.file, before: "", after: test.content, notes: ["generated contract test"] };
@@ -2398,6 +3528,42 @@ function rejected(ws, test, baseline, attempts) {
2398
3528
  async function restore(ws, changes) {
2399
3529
  for (const c of changes) await ws.write(c.file, c.before);
2400
3530
  }
3531
+ async function repairAdvisory(input) {
3532
+ if (!input.workspace && !input.repoDir) throw new Error("repairAdvisory needs a workspace or a repoDir");
3533
+ const ws = input.workspace ?? localWorkspace(input.repoDir);
3534
+ const seen = /* @__PURE__ */ new Set();
3535
+ const order = input.callSites.filter((s) => {
3536
+ const key = `${s.file}:${s.line}:${s.column}`;
3537
+ return !seen.has(key) && Boolean(seen.add(key));
3538
+ }).sort((a, b) => a.file.localeCompare(b.file) || b.line - a.line || b.column - a.column);
3539
+ const originals = /* @__PURE__ */ new Map();
3540
+ const sites = [];
3541
+ for (const [i, callSite] of order.entries()) {
3542
+ if (!originals.has(callSite.file)) originals.set(callSite.file, await ws.read(callSite.file) ?? "");
3543
+ const testFile = `test/shapi/${input.advisory.id}${i === 0 ? "" : `.${i + 1}`}.test.ts`;
3544
+ const result = await repairCallSite({ ...input, workspace: ws, callSite, testFile });
3545
+ for (const c of result.changes) if (c.before && !originals.has(c.file)) originals.set(c.file, c.before);
3546
+ sites.push({ callSite, result });
3547
+ }
3548
+ const fixed = sites.filter((s) => s.result.status === "fixed");
3549
+ const needed = sites.filter((s) => s.result.status !== "already-handled");
3550
+ const tests = new Set(fixed.map((s) => s.result.test?.file));
3551
+ const sourceFiles = [...new Set(fixed.flatMap((s) => s.result.changes.filter((c) => !tests.has(c.file)).map((c) => c.file)))].sort();
3552
+ const changes = [];
3553
+ for (const file of sourceFiles) {
3554
+ const notes = fixed.flatMap((s) => s.result.changes.filter((c) => c.file === file).flatMap((c) => c.notes));
3555
+ changes.push({ file, before: originals.get(file) ?? "", after: await ws.read(file) ?? "", notes });
3556
+ }
3557
+ for (const s of fixed) if (s.result.test) changes.push({ file: s.result.test.file, before: "", after: s.result.test.content, notes: ["generated contract test"] });
3558
+ const methods = new Set(fixed.map((s) => s.result.method));
3559
+ return {
3560
+ status: fixed.length === 0 ? "none" : fixed.length === needed.length ? "fixed" : "partial",
3561
+ sites,
3562
+ changes,
3563
+ ...fixed.length ? { method: methods.has("llm") ? "llm" : "recipe" } : {},
3564
+ sandbox: ws.location
3565
+ };
3566
+ }
2401
3567
 
2402
3568
  // packages/repair/src/llm.ts
2403
3569
  import Anthropic from "@anthropic-ai/sdk";
@@ -2510,7 +3676,8 @@ ${req.failingOutput.slice(-6e3)}
2510
3676
 
2511
3677
  // packages/repair/src/index.ts
2512
3678
  var RECIPES = {
2513
- [statusCodeAcceptSet.id]: statusCodeAcceptSet
3679
+ [statusCodeAcceptSet.id]: statusCodeAcceptSet,
3680
+ [applyFix.id]: applyFix
2514
3681
  };
2515
3682
 
2516
3683
  // packages/github-app/src/pr.ts
@@ -2651,29 +3818,31 @@ function prTitle(advisory) {
2651
3818
  function prBranch(advisory) {
2652
3819
  return `shapi/${advisory.id}`;
2653
3820
  }
2654
- function renderPrBody({ advisory, callSite, result, affectedInRepo, shapeSource }) {
2655
- const source = result.changes.filter((c) => c.file !== result.test?.file);
2656
- const notes = source.flatMap((c) => c.notes.map((n) => `- ${n}`));
2657
- const method = result.method === "recipe" ? `deterministic recipe \`${advisory.recipeId}\`` : "Claude, verified by the contract test";
3821
+ function renderPrBody({ advisory, repair, shapeSource }) {
3822
+ const fixed = repair.sites.filter((s) => s.result.status === "fixed");
3823
+ const tests = new Set(fixed.map((s) => s.result.test?.file));
3824
+ const notes = repair.changes.filter((c) => !tests.has(c.file)).flatMap((c) => (c.notes.length ? c.notes : [`\`${c.file}\``]).map((n) => `- ${n}`));
3825
+ const method = repair.method === "llm" ? "Claude, verified by the contract tests" : `deterministic recipe \`${advisory.recipeId}\``;
2658
3826
  const evidence = advisory.evidence;
3827
+ const where = (s) => `\`${s.callSite.file}:${s.callSite.line}\` (${s.callSite.method} ${s.callSite.endpoint})`;
3828
+ const siteLine = (s) => s.result.status === "fixed" ? `- [x] ${where(s)}: fixed, test \`${s.result.test?.file}\`` : s.result.status === "already-handled" ? `- [x] ${where(s)}: already handles the change` : `- [ ] ${where(s)}: not fixed here (${s.result.reason ?? s.result.status}); needs a manual change`;
2659
3829
  const lines = [
2660
3830
  `**${advisory.provider}** changed \`${advisory.endpoint.method} ${advisory.endpoint.path}\`: ${advisory.title}.`,
2661
3831
  "",
2662
3832
  advisory.summary,
2663
3833
  "",
2664
- "### Affected call site",
2665
- `\`${callSite.file}:${callSite.line}\` (${callSite.method} ${callSite.endpoint})`,
3834
+ `### Affected call sites (${fixed.length} of ${repair.sites.length} fixed here)`,
3835
+ ...repair.sites.map(siteLine),
2666
3836
  "",
2667
3837
  `### Change (${method})`,
2668
- ...notes.length ? notes : source.map((c) => `- \`${c.file}\``),
3838
+ ...notes,
2669
3839
  "",
2670
- "### Contract test",
2671
- `Added \`${result.test?.file}\`. It calls the code with the response it was written for and the response ${advisory.provider} sends now.`,
3840
+ `### Contract test${fixed.length === 1 ? "" : "s"}`,
3841
+ "Each test calls the code with the response it was written for and the one the provider sends now. Each failed before this change and passes after it.",
2672
3842
  "",
2673
- "| Run | Result |",
2674
- "|---|---|",
2675
- `| Before this change | ${summary(result.baseline)} |`,
2676
- `| After this change | ${summary(result.attempts.find((a) => a.outcome === "passed")?.run)} |`,
3843
+ "| Test | Before this change | After this change |",
3844
+ "|---|---|---|",
3845
+ ...fixed.map((s) => `| \`${s.result.test?.file}\` | ${summary(s.result.baseline)} | ${summary(s.result.attempts.find((a) => a.outcome === "passed")?.run)} |`),
2677
3846
  "",
2678
3847
  "### Evidence",
2679
3848
  "| | |",
@@ -2685,13 +3854,13 @@ function renderPrBody({ advisory, callSite, result, affectedInRepo, shapeSource
2685
3854
  `| Provider notice | ${advisory.publishedNotice ? `[${advisory.publishedNotice.date ?? "link"}](${advisory.publishedNotice.url})` : "none (undocumented change)"} |`,
2686
3855
  `| Shapes from | ${shapeSource} |`,
2687
3856
  ...evidence ? [`| Observed | ${evidence.before.status} at ${evidence.before.observedAt} \u2192 ${evidence.after.status} at ${evidence.after.observedAt} (${evidence.source}) |`] : [],
2688
- `| Blast radius | ${affectedInRepo} affected call site${affectedInRepo === 1 ? "" : "s"} in this repo |`,
3857
+ `| Blast radius | ${repair.sites.length} affected call site${repair.sites.length === 1 ? "" : "s"} in this repo |`,
2689
3858
  `| Sources | ${advisory.sources.map((s, i) => `[${i + 1}](${s})`).join(" ")} |`,
2690
3859
  "",
2691
3860
  `Migration note: ${advisory.migrationNote}`,
2692
3861
  "",
2693
3862
  "---",
2694
- "Opened by [SHAPI](https://github.com/SriVarshini09/YC). The change was kept only because the contract test failed before it and passes after it."
3863
+ "Opened by [SHAPI](https://github.com/SriVarshini09/YC). A change is kept only when its contract test failed before it and passes after it."
2695
3864
  ];
2696
3865
  return lines.join("\n") + "\n";
2697
3866
  }
@@ -2712,7 +3881,7 @@ import { Octokit as Octokit2 } from "@octokit/rest";
2712
3881
  var ResilientOctokit = Octokit2.plugin(throttling, retry);
2713
3882
 
2714
3883
  // packages/github-app/src/snapshot.ts
2715
- import { Parser } from "tar";
3884
+ import { Parser as Parser2 } from "tar";
2716
3885
 
2717
3886
  // packages/cli/src/scan.ts
2718
3887
  import { execFileSync as execFileSync2 } from "child_process";
@@ -2721,16 +3890,19 @@ import os2 from "os";
2721
3890
  import path11 from "path";
2722
3891
 
2723
3892
  // packages/cli/src/paths.ts
2724
- import { existsSync as existsSync3 } from "fs";
2725
- import { fileURLToPath } from "url";
3893
+ import { existsSync as existsSync4 } from "fs";
3894
+ import { fileURLToPath as fileURLToPath2 } from "url";
2726
3895
  function bundledDir(published, monorepo) {
2727
- const candidates = [new URL(`../${published}`, import.meta.url), new URL(`../../../${monorepo}`, import.meta.url)].map((u) => fileURLToPath(u));
2728
- return candidates.find((p) => existsSync3(p)) ?? candidates[candidates.length - 1];
3896
+ const candidates = [new URL(`../${published}`, import.meta.url), new URL(`../../../${monorepo}`, import.meta.url)].map((u) => fileURLToPath2(u));
3897
+ return candidates.find((p) => existsSync4(p)) ?? candidates[candidates.length - 1];
2729
3898
  }
2730
3899
 
2731
3900
  // packages/cli/src/scan.ts
2732
- var VERSION = "0.1.0";
3901
+ var VERSION = "0.2.0";
2733
3902
  var DEFAULT_ADVISORIES = bundledDir("advisories", "advisories");
3903
+ async function preparePython() {
3904
+ await loadPythonParser().catch(() => void 0);
3905
+ }
2734
3906
  function scan(target, opts) {
2735
3907
  const started = performance.now();
2736
3908
  const failOn = parseFailOn(opts.failOn ?? "critical");
@@ -2756,6 +3928,7 @@ function scan(target, opts) {
2756
3928
  return {
2757
3929
  label: label2,
2758
3930
  filesScanned: extracted.filesScanned,
3931
+ skipped: extracted.skipped,
2759
3932
  lock,
2760
3933
  match,
2761
3934
  elapsedMs: Math.round(performance.now() - started),
@@ -2822,24 +3995,31 @@ async function fix(target, opts, deps) {
2822
3995
  if (affected.length === 0) deps.log("No affected call sites: nothing to fix.");
2823
3996
  const llm = opts.llm ? claudeFixer() : void 0;
2824
3997
  const outcomes = [];
2825
- for (const finding of affected) {
2826
- const { advisory, callSite } = finding;
2827
- const where = `${callSite.file}:${callSite.line}`;
2828
- const shapes = findShapes(advisory, opts);
3998
+ const byAdvisory = /* @__PURE__ */ new Map();
3999
+ for (const f of affected) byAdvisory.set(f.advisory.id, [...byAdvisory.get(f.advisory.id) ?? [], f]);
4000
+ for (const findings of byAdvisory.values()) {
4001
+ const advisory = findings[0].advisory;
4002
+ const shapes = advisory.fix ? { source: FIX_SOURCE } : findShapes(advisory, opts);
2829
4003
  if (!shapes) {
2830
4004
  const skipped = "no before/after shapes for this endpoint (record them with the canary, or add a recording)";
2831
- deps.log(`SKIP ${where} ${advisory.id}: ${skipped}`);
2832
- outcomes.push({ finding, skipped });
4005
+ for (const finding of findings) {
4006
+ deps.log(`SKIP ${finding.callSite.file}:${finding.callSite.line} ${advisory.id}: ${skipped}`);
4007
+ outcomes.push({ finding, skipped });
4008
+ }
2833
4009
  continue;
2834
4010
  }
2835
- const result = await repairCallSite({ repoDir: dir, advisory, callSite, before: shapes.before, after: shapes.after, recipes: RECIPES, llm });
2836
- const outcome = { finding, shapeSource: shapes.source, result };
2837
- outcomes.push(outcome);
2838
- deps.log(`${result.status.toUpperCase().padEnd(15)} ${where} ${advisory.id}${result.method ? ` via ${result.method}` : ""}${result.reason ? `: ${result.reason}` : ""}`);
2839
- for (const a of result.attempts) deps.log(` ${a.method}: ${a.outcome} (${a.detail})`);
2840
- if (result.status !== "fixed") continue;
2841
- for (const c of result.changes) deps.log(` ${c.before ? "M" : "A"} ${c.file}`);
2842
- const files = result.changes.map((c) => ({ path: c.file, content: c.after }));
4011
+ const repair = await repairAdvisory({ workspace: localWorkspace(dir), advisory, callSites: findings.map((f) => f.callSite), before: shapes.before, after: shapes.after, recipes: RECIPES, llm });
4012
+ const mine = [];
4013
+ for (const { callSite, result } of repair.sites) {
4014
+ const finding = findings.find((f) => f.callSite.file === callSite.file && f.callSite.line === callSite.line && f.callSite.column === callSite.column);
4015
+ mine.push({ finding, shapeSource: shapes.source, result });
4016
+ deps.log(`${result.status.toUpperCase().padEnd(15)} ${callSite.file}:${callSite.line} ${advisory.id}${result.method ? ` via ${result.method}` : ""}${result.reason ? `: ${result.reason}` : ""}`);
4017
+ for (const a of result.attempts) deps.log(` ${a.method}: ${a.outcome} (${a.detail})`);
4018
+ }
4019
+ outcomes.push(...mine);
4020
+ if (!repair.changes.length) continue;
4021
+ for (const c of repair.changes) deps.log(` ${c.before ? "M" : "A"} ${c.file}`);
4022
+ const files = repair.changes.map((c) => ({ path: c.file, content: c.after }));
2843
4023
  if (opts.write) {
2844
4024
  for (const f of files) {
2845
4025
  mkdirSync5(path12.dirname(path12.join(dir, f.path)), { recursive: true });
@@ -2848,22 +4028,18 @@ async function fix(target, opts, deps) {
2848
4028
  deps.log(` wrote ${files.length} files into ${dir}`);
2849
4029
  }
2850
4030
  if (opts.pr && remote) {
4031
+ const fixed = repair.sites.filter((s) => s.result.status === "fixed");
2851
4032
  const meta = {
2852
4033
  title: prTitle(advisory),
2853
4034
  branch: prBranch(advisory),
2854
4035
  commitMessage: `fix: ${advisory.title}
2855
4036
 
2856
- Advisory ${advisory.id}. Contract test ${result.test?.file} fails before this change and passes after.`,
2857
- body: renderPrBody({
2858
- advisory,
2859
- callSite,
2860
- result,
2861
- affectedInRepo: affected.filter((f) => f.advisory.id === advisory.id).length,
2862
- shapeSource: shapes.source
2863
- })
4037
+ Advisory ${advisory.id}, ${fixed.length} of ${repair.sites.length} call sites. Contract tests ${fixed.map((s) => s.result.test?.file).join(", ")} fail before this change and pass after.`,
4038
+ body: renderPrBody({ advisory, repair, shapeSource: shapes.source })
2864
4039
  };
2865
- outcome.pr = await (deps.deliver ?? deliverToGitHub)(remote, files, meta);
2866
- deps.log(` pull request: ${outcome.pr.url}`);
4040
+ const pr = await (deps.deliver ?? deliverToGitHub)(remote, files, meta);
4041
+ for (const o of mine) if (o.result?.status === "fixed") o.pr = pr;
4042
+ deps.log(` pull request: ${pr.url}`);
2867
4043
  }
2868
4044
  }
2869
4045
  return outcomes;
@@ -2878,8 +4054,9 @@ async function deliverToGitHub(repo, files, meta) {
2878
4054
  if (outcome.action === "skipped") throw new Error(`not delivered: ${outcome.reason}${outcome.url ? ` (${outcome.url})` : ""}`);
2879
4055
  return { url: outcome.url, number: outcome.number, branch: outcome.branch, commit: outcome.commit };
2880
4056
  }
4057
+ var FIX_SOURCE = "built from the advisory's fix and the fields the code reads";
2881
4058
  function findShapes(advisory, opts) {
2882
- if (opts.db && existsSync4(opts.db)) {
4059
+ if (opts.db && existsSync5(opts.db)) {
2883
4060
  const store = ShapeStore.open(opts.db);
2884
4061
  try {
2885
4062
  for (const e of store.endpoints()) {
@@ -2894,7 +4071,7 @@ function findShapes(advisory, opts) {
2894
4071
  }
2895
4072
  }
2896
4073
  const dir = opts.recordings ?? DEFAULT_RECORDINGS;
2897
- if (!existsSync4(dir)) return void 0;
4074
+ if (!existsSync5(dir)) return void 0;
2898
4075
  for (const name of readdirSync3(dir).filter((f) => f.endsWith(".json")).sort()) {
2899
4076
  const recording = loadRecording(path12.join(dir, name));
2900
4077
  if (recording.provider !== advisory.provider || !pathMatches(recording.endpoint, advisory.endpoint)) continue;
@@ -2929,6 +4106,7 @@ function renderText(o, color) {
2929
4106
  [plural(o.filesScanned, "file"), plural(lock.callSites.length, "call site"), plural(providers.length, "provider"), `${o.elapsedMs} ms`].join(" \xB7 ")
2930
4107
  )
2931
4108
  );
4109
+ if (o.skipped.length) out.push(paint("dim", `${plural(o.skipped.length, "file")} not scanned: ${[...new Set(o.skipped.map((s) => s.reason))].join("; ")}`));
2932
4110
  out.push("");
2933
4111
  out.push(paint("bold", "Providers"));
2934
4112
  if (providers.length === 0) out.push(" none found");
@@ -2988,6 +4166,7 @@ var processIo = {
2988
4166
  };
2989
4167
  async function run(argv, io = processIo, fixDeps = {}) {
2990
4168
  let exitCode = 0;
4169
+ await preparePython();
2991
4170
  const program = new Command2().name("shapi").description("Map the third-party APIs a codebase depends on and flag the breaking changes that hit it.").version(VERSION).exitOverride().configureOutput({ writeOut: io.stdout, writeErr: io.stderr });
2992
4171
  program.command("scan").description("Scan a repository and report affected call sites").argument("[path]", "directory to scan", ".").option("--json", "print a JSON report (includes the api.lock)").option("--out <file>", "write the api.lock to this file").option("--repo <url>", "clone a git repository (shallow) into a temp dir and scan it").option("--advisories <dir>", "advisory directory (default: the bundled advisories/)").option("--fail-on <severity>", "exit 1 when an affected call site is at least this severe (critical|high|medium|low|none)", "critical").option("--no-color", "disable color").addOption(new Option("--now <date>", "evaluate deadlines as of this date").hideHelp()).action((target, opts) => {
2993
4172
  const outcome = scan(opts.repo ? void 0 : target, opts);
@@ -3019,6 +4198,7 @@ export {
3019
4198
  VERSION,
3020
4199
  fix,
3021
4200
  main,
4201
+ preparePython,
3022
4202
  renderText,
3023
4203
  run,
3024
4204
  scan,