@databricks-solutions/lakebase-scm-utils 0.3.10 → 0.3.11

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 (48) hide show
  1. package/dist/{pr-e-E4aP4j.d.cts → pr-D-eayWyJ.d.cts} +59 -1
  2. package/dist/{pr-e-E4aP4j.d.ts → pr-D-eayWyJ.d.ts} +59 -1
  3. package/dist/scripts/git/index.cjs +30 -14
  4. package/dist/scripts/git/index.d.cts +32 -1
  5. package/dist/scripts/git/index.d.ts +32 -1
  6. package/dist/scripts/git/index.js +28 -14
  7. package/dist/scripts/github/index.d.cts +2 -60
  8. package/dist/scripts/github/index.d.ts +2 -60
  9. package/dist/scripts/index.cjs +244 -76
  10. package/dist/scripts/index.d.cts +4 -4
  11. package/dist/scripts/index.d.ts +4 -4
  12. package/dist/scripts/index.js +239 -76
  13. package/dist/scripts/lakebase/ci-resolve-branch.cli.cjs +2 -2
  14. package/dist/scripts/lakebase/ci-resolve-branch.cli.js +2 -2
  15. package/dist/scripts/lakebase/collapse-heads.cli.cjs +2 -2
  16. package/dist/scripts/lakebase/collapse-heads.cli.js +2 -2
  17. package/dist/scripts/lakebase/detect-language.cli.cjs +2 -2
  18. package/dist/scripts/lakebase/detect-language.cli.js +2 -2
  19. package/dist/scripts/lakebase/doctor.cli.cjs +2 -2
  20. package/dist/scripts/lakebase/doctor.cli.js +2 -2
  21. package/dist/scripts/lakebase/get-connection.cli.cjs +2 -2
  22. package/dist/scripts/lakebase/get-connection.cli.js +2 -2
  23. package/dist/scripts/lakebase/index.cjs +256 -77
  24. package/dist/scripts/lakebase/index.d.cts +74 -2
  25. package/dist/scripts/lakebase/index.d.ts +74 -2
  26. package/dist/scripts/lakebase/index.js +253 -77
  27. package/dist/scripts/lakebase/infra-runner.cli.cjs +2 -2
  28. package/dist/scripts/lakebase/infra-runner.cli.js +2 -2
  29. package/dist/scripts/lakebase/new-migration.cli.cjs +2 -2
  30. package/dist/scripts/lakebase/new-migration.cli.js +2 -2
  31. package/dist/scripts/lakebase/schema-diff.cli.cjs +2 -2
  32. package/dist/scripts/lakebase/schema-diff.cli.js +2 -2
  33. package/dist/scripts/lakebase/schema-migrate.cli.cjs +2 -2
  34. package/dist/scripts/lakebase/schema-migrate.cli.js +2 -2
  35. package/dist/scripts/lakebase/scm-claim-feature.cli.cjs +2 -2
  36. package/dist/scripts/lakebase/scm-claim-feature.cli.js +2 -2
  37. package/dist/scripts/lakebase/scm-doctor.cli.cjs +2 -2
  38. package/dist/scripts/lakebase/scm-doctor.cli.js +2 -2
  39. package/dist/scripts/lakebase/scm-merge.cli.cjs +2 -2
  40. package/dist/scripts/lakebase/scm-merge.cli.js +2 -2
  41. package/dist/scripts/lakebase/scm-reconcile-tier.cli.cjs +2 -2
  42. package/dist/scripts/lakebase/scm-reconcile-tier.cli.js +2 -2
  43. package/dist/scripts/util/index.cjs +2 -2
  44. package/dist/scripts/util/index.js +2 -2
  45. package/package.json +1 -1
  46. package/scripts/git/commit-push.ts +60 -14
  47. package/scripts/lakebase/create-preflight.ts +213 -1
  48. package/scripts/lakebase/create-project.ts +98 -30
@@ -427,8 +427,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
427
427
  var cached;
428
428
  function substrateSelfVersion() {
429
429
  if (cached !== void 0) return cached;
430
- if ("0.3.10".length > 0) {
431
- cached = "0.3.10";
430
+ if ("0.3.11".length > 0) {
431
+ cached = "0.3.11";
432
432
  return cached;
433
433
  }
434
434
  cached = "unknown";
@@ -79816,8 +79816,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
79816
79816
  var cached;
79817
79817
  function substrateSelfVersion() {
79818
79818
  if (cached !== void 0) return cached;
79819
- if ("0.3.10".length > 0) {
79820
- cached = "0.3.10";
79819
+ if ("0.3.11".length > 0) {
79820
+ cached = "0.3.11";
79821
79821
  return cached;
79822
79822
  }
79823
79823
  cached = "unknown";
@@ -897,8 +897,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
897
897
  var cached;
898
898
  function substrateSelfVersion() {
899
899
  if (cached !== void 0) return cached;
900
- if ("0.3.10".length > 0) {
901
- cached = "0.3.10";
900
+ if ("0.3.11".length > 0) {
901
+ cached = "0.3.11";
902
902
  return cached;
903
903
  }
904
904
  cached = "unknown";
@@ -88273,8 +88273,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
88273
88273
  var cached;
88274
88274
  function substrateSelfVersion() {
88275
88275
  if (cached !== void 0) return cached;
88276
- if ("0.3.10".length > 0) {
88277
- cached = "0.3.10";
88276
+ if ("0.3.11".length > 0) {
88277
+ cached = "0.3.11";
88278
88278
  return cached;
88279
88279
  }
88280
88280
  cached = "unknown";
@@ -9426,8 +9426,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
9426
9426
  var cached;
9427
9427
  function substrateSelfVersion() {
9428
9428
  if (cached !== void 0) return cached;
9429
- if ("0.3.10".length > 0) {
9430
- cached = "0.3.10";
9429
+ if ("0.3.11".length > 0) {
9430
+ cached = "0.3.11";
9431
9431
  return cached;
9432
9432
  }
9433
9433
  cached = "unknown";
@@ -88825,8 +88825,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
88825
88825
  var cached;
88826
88826
  function substrateSelfVersion() {
88827
88827
  if (cached !== void 0) return cached;
88828
- if ("0.3.10".length > 0) {
88829
- cached = "0.3.10";
88828
+ if ("0.3.11".length > 0) {
88829
+ cached = "0.3.11";
88830
88830
  return cached;
88831
88831
  }
88832
88832
  cached = "unknown";
@@ -9978,8 +9978,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
9978
9978
  var cached;
9979
9979
  function substrateSelfVersion() {
9980
9980
  if (cached !== void 0) return cached;
9981
- if ("0.3.10".length > 0) {
9982
- cached = "0.3.10";
9981
+ if ("0.3.11".length > 0) {
9982
+ cached = "0.3.11";
9983
9983
  return cached;
9984
9984
  }
9985
9985
  cached = "unknown";
@@ -79288,8 +79288,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
79288
79288
  var cached;
79289
79289
  function substrateSelfVersion() {
79290
79290
  if (cached !== void 0) return cached;
79291
- if ("0.3.10".length > 0) {
79292
- cached = "0.3.10";
79291
+ if ("0.3.11".length > 0) {
79292
+ cached = "0.3.11";
79293
79293
  return cached;
79294
79294
  }
79295
79295
  cached = "unknown";
@@ -389,8 +389,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
389
389
  var cached;
390
390
  function substrateSelfVersion() {
391
391
  if (cached !== void 0) return cached;
392
- if ("0.3.10".length > 0) {
393
- cached = "0.3.10";
392
+ if ("0.3.11".length > 0) {
393
+ cached = "0.3.11";
394
394
  return cached;
395
395
  }
396
396
  cached = "unknown";
@@ -1667,8 +1667,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
1667
1667
  var cached;
1668
1668
  function substrateSelfVersion() {
1669
1669
  if (cached !== void 0) return cached;
1670
- if ("0.3.10".length > 0) {
1671
- cached = "0.3.10";
1670
+ if ("0.3.11".length > 0) {
1671
+ cached = "0.3.11";
1672
1672
  return cached;
1673
1673
  }
1674
1674
  cached = "unknown";
@@ -1630,8 +1630,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
1630
1630
  var cached;
1631
1631
  function substrateSelfVersion() {
1632
1632
  if (cached !== void 0) return cached;
1633
- if ("0.3.10".length > 0) {
1634
- cached = "0.3.10";
1633
+ if ("0.3.11".length > 0) {
1634
+ cached = "0.3.11";
1635
1635
  return cached;
1636
1636
  }
1637
1637
  cached = "unknown";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@databricks-solutions/lakebase-scm-utils",
3
- "version": "0.3.10",
3
+ "version": "0.3.11",
4
4
  "description": "Lakebase SCM + substrate engine: database branching, paired-branch SCM workflows, connection/credential minting, schema migration, project scaffold + deploy primitives, and the git/github utility layer. Extracted from consort for portable, standalone consumption by lakebase-scm-extension and the SFTDD orchestration.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -45,24 +45,70 @@ export class WorkflowScopeError extends Error {
45
45
  * For non-initial-commit flows, prefer the primitives in ./commits.ts.
46
46
  */
47
47
  export async function commitAndPush(args: CommitAndPushArgs): Promise<void> {
48
- await exec("git add -A", { cwd: args.projectDir });
49
- await exec(`git commit -m ${JSON.stringify(args.message)}`, {
50
- cwd: args.projectDir,
48
+ await commitInitial(args.projectDir, args.message);
49
+ if (args.push === false) return;
50
+ await pushInitial({ projectDir: args.projectDir, remote: args.remote, branch: args.branch });
51
+ }
52
+
53
+ /** Stage everything and make the initial commit (no push). */
54
+ export async function commitInitial(projectDir: string, message: string): Promise<void> {
55
+ await exec("git add -A", { cwd: projectDir });
56
+ await exec(`git commit -m ${JSON.stringify(message)}`, {
57
+ cwd: projectDir,
51
58
  timeout: 30_000,
52
59
  });
53
- if (args.push === false) return;
60
+ }
61
+
62
+ export interface PushInitialArgs {
63
+ projectDir: string;
64
+ /** Remote name. Default: "origin". */
65
+ remote?: string;
66
+ /** Branch name. Default: "main". */
67
+ branch?: string;
68
+ /**
69
+ * Total push attempts (>= 1). Default 1 (preserves commitAndPush's behavior).
70
+ * The create flow passes 2: the FIRST push fires the project's pre-push hook,
71
+ * which triggers the one-time cold download of the Consort toolkit — that race
72
+ * can fail the push, but the toolkit is CACHED afterward, so a single retry then
73
+ * sails through (the cold-download incident). A `workflow`-scope rejection is a
74
+ * real auth gap, never retried.
75
+ */
76
+ attempts?: number;
77
+ /** Delay between attempts (ms). Default 1500. Injected in tests. */
78
+ delayMs?: number;
79
+ /** Injectable sleep (tests). */
80
+ sleep?: (ms: number) => Promise<void>;
81
+ /** Injectable runner (tests); defaults to the real `git push`. */
82
+ run?: (cmd: string, cwd: string) => Promise<void>;
83
+ }
84
+
85
+ /**
86
+ * Push the initial branch with `-u`, retrying a transient failure up to
87
+ * `attempts` times. Maps the GitHub `workflow`-scope rejection to
88
+ * {@link WorkflowScopeError} (thrown immediately, never retried). Throws the last
89
+ * error after the final attempt.
90
+ */
91
+ export async function pushInitial(args: PushInitialArgs): Promise<void> {
54
92
  const remote = args.remote ?? "origin";
55
93
  const branch = args.branch ?? "main";
56
- try {
57
- await exec(`git push -u ${remote} ${branch}`, {
58
- cwd: args.projectDir,
59
- timeout: 30_000,
60
- });
61
- } catch (err) {
62
- const msg = err instanceof Error ? err.message : String(err);
63
- if (/without `?workflow`? scope|workflow scope/i.test(msg)) {
64
- throw new WorkflowScopeError(args.projectDir);
94
+ const attempts = Math.max(1, args.attempts ?? 1);
95
+ const sleep = args.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
96
+ const run =
97
+ args.run ??
98
+ ((cmd: string, cwd: string) => exec(cmd, { cwd, timeout: 30_000 }).then(() => undefined));
99
+ let lastErr: unknown;
100
+ for (let i = 1; i <= attempts; i++) {
101
+ try {
102
+ await run(`git push -u ${remote} ${branch}`, args.projectDir);
103
+ return;
104
+ } catch (err) {
105
+ const msg = err instanceof Error ? err.message : String(err);
106
+ if (/without `?workflow`? scope|workflow scope/i.test(msg)) {
107
+ throw new WorkflowScopeError(args.projectDir);
108
+ }
109
+ lastErr = err;
110
+ if (i < attempts) await sleep(args.delayMs ?? 1500);
65
111
  }
66
- throw err;
67
112
  }
113
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
68
114
  }
@@ -19,7 +19,7 @@ import * as fs from "node:fs";
19
19
  import * as path from "node:path";
20
20
  import { deleteLakebaseProject } from "./lakebase-project.js";
21
21
  import { runDatabricks } from "./databricks-cli.js";
22
- import { checkGitHubScopes } from "../github/repo.js";
22
+ import { checkGitHubScopes, getCurrentUser, repoExists, type GitHubScopeStatus } from "../github/repo.js";
23
23
 
24
24
  export interface PreflightResult {
25
25
  ok: boolean;
@@ -145,6 +145,134 @@ export async function checkGitHubWorkflowScope(
145
145
  }
146
146
  }
147
147
 
148
+ export interface GitHubPreflightInput {
149
+ /** Target repo owner (user or org). */
150
+ owner: string;
151
+ /** Bare repo name (the project name). */
152
+ repoName: string;
153
+ /** Scopes whose absence BLOCKS the happy path (create + push). Default ["repo","workflow"]. */
154
+ hardScopes?: readonly string[];
155
+ /** Scopes whose absence only WARNS (e.g. delete_repo, needed for rollback repo-delete). Default ["delete_repo"]. */
156
+ advisoryScopes?: readonly string[];
157
+ /** Injected probes (default to the real GitHub reads). */
158
+ currentUser?: () => Promise<string>;
159
+ scopes?: (required: readonly string[]) => Promise<GitHubScopeStatus>;
160
+ repoExists?: (fullName: string) => Promise<boolean>;
161
+ }
162
+
163
+ export interface GitHubPreflightResult extends PreflightResult {
164
+ /** Non-blocking notes (org/owner mismatch, missing advisory scope, invisible scopes). */
165
+ warnings: string[];
166
+ /** The resolved authenticated login, when B1 succeeded. */
167
+ activeUser?: string;
168
+ }
169
+
170
+ /**
171
+ * One up-front GitHub preflight run BEFORE any repo / Lakebase project is created,
172
+ * so a missing requisite fails in seconds with a fix instead of mid-create (where
173
+ * it orphans partially-created artifacts). Supersedes the workflow-only check:
174
+ *
175
+ * B1 active account getCurrentUser() must resolve (a valid token) — else a
176
+ * wrong/absent token otherwise 403s LATE at the push.
177
+ * B2 scope set checkGitHubScopes([...hard, ...advisory]): a missing HARD
178
+ * scope (repo/workflow) fails; a missing ADVISORY scope
179
+ * (delete_repo) warns (rollback repo-delete unavailable);
180
+ * granted null (fine-grained PAT / App) can't be asserted.
181
+ * B3 name availability repoExists(owner/name) → fail before anything exists.
182
+ * B4 owner note login != owner ⇒ creating under an org/other account —
183
+ * surfaced (createRepo enforces the real org-create right).
184
+ *
185
+ * CONSERVATIVE: hard-fails ONLY on the clear blockers (invalid token, missing
186
+ * happy-path scope, name taken); every other uncertainty is a warning, never a
187
+ * false block (same ethos as checkGitHubWorkflowScope). All probes injected, so
188
+ * this is unit-testable with no network.
189
+ */
190
+ export async function checkGitHubPreflight(input: GitHubPreflightInput): Promise<GitHubPreflightResult> {
191
+ const warnings: string[] = [];
192
+ const hard = input.hardScopes ?? ["repo", "workflow"];
193
+ const advisory = input.advisoryScopes ?? ["delete_repo"];
194
+ const currentUserProbe = input.currentUser ?? getCurrentUser;
195
+ const scopesProbe = input.scopes ?? ((req: readonly string[]) => checkGitHubScopes(req));
196
+ const repoExistsProbe = input.repoExists ?? repoExists;
197
+
198
+ // B1: active account — a valid token. Hard blocker; nothing downstream works without it.
199
+ let activeUser: string;
200
+ try {
201
+ activeUser = await currentUserProbe();
202
+ } catch (err) {
203
+ return {
204
+ ok: false,
205
+ warnings,
206
+ reason:
207
+ `GitHub authentication failed: ${err instanceof Error ? err.message : String(err)}. ` +
208
+ "Sign in with `gh auth login`, or set GITHUB_TOKEN to a valid token, then re-run.",
209
+ };
210
+ }
211
+
212
+ // B4 (note): a login different from the owner means an org (or another account).
213
+ // Not a hard block — createRepo surfaces the real org-create permission — but
214
+ // surface it so a wrong active account is visible BEFORE the push's late 403.
215
+ if (activeUser.toLowerCase() !== input.owner.toLowerCase()) {
216
+ warnings.push(
217
+ `Signed in to GitHub as "${activeUser}" but creating under "${input.owner}" (an org or another account); ` +
218
+ `ensure "${activeUser}" can create repositories there.`,
219
+ );
220
+ }
221
+
222
+ // B2: the full required scope set — not just `workflow`.
223
+ try {
224
+ const { granted, missing } = await scopesProbe([...hard, ...advisory]);
225
+ if (granted !== null) {
226
+ const missingHard = missing.filter((s) => hard.includes(s));
227
+ const missingAdvisory = missing.filter((s) => advisory.includes(s));
228
+ if (missingHard.length > 0) {
229
+ return {
230
+ ok: false,
231
+ warnings,
232
+ activeUser,
233
+ reason:
234
+ `GitHub token missing required scope(s): ${missingHard.join(", ")} ` +
235
+ `(grants [${granted.join(", ") || "none"}]). Fix: gh auth refresh -h github.com -s ${[...hard].join(",")}`,
236
+ };
237
+ }
238
+ if (missingAdvisory.length > 0) {
239
+ warnings.push(
240
+ `GitHub token missing the ${missingAdvisory.join(", ")} scope — automatic rollback of the repo on a failed ` +
241
+ "create is unavailable (you would delete it by hand). Grant it with: gh auth refresh -h github.com -s delete_repo",
242
+ );
243
+ }
244
+ } else {
245
+ warnings.push(
246
+ "GitHub token scopes are not visible (fine-grained PAT or App token); cannot pre-verify repo/workflow access , " +
247
+ "the create will surface any gap when it pushes.",
248
+ );
249
+ }
250
+ } catch {
251
+ warnings.push("Could not read GitHub token scopes (probe error); skipping the scope pre-check.");
252
+ }
253
+
254
+ // B3: name availability — fail before anything is created.
255
+ try {
256
+ if (await repoExistsProbe(`${input.owner}/${input.repoName}`)) {
257
+ return {
258
+ ok: false,
259
+ warnings,
260
+ activeUser,
261
+ reason:
262
+ `GitHub repo "${input.owner}/${input.repoName}" already exists. ` +
263
+ "Pick a different project name, or delete the existing repo first.",
264
+ };
265
+ }
266
+ } catch {
267
+ warnings.push(
268
+ `Could not check whether "${input.owner}/${input.repoName}" already exists (probe error); ` +
269
+ "the create will fail clearly if it does.",
270
+ );
271
+ }
272
+
273
+ return { ok: true, warnings, activeUser };
274
+ }
275
+
148
276
  /** The actionable prereq message for a GitHub token missing the `workflow` scope. */
149
277
  export function githubWorkflowScopePrereqMessage(reason?: string): string {
150
278
  return (
@@ -258,3 +386,87 @@ export async function withLakebaseRollback<T>(opts: RollbackOptions, fn: () => P
258
386
  throw wrapped;
259
387
  }
260
388
  }
389
+
390
+ export interface RollbackStep {
391
+ label: string;
392
+ undo: () => Promise<void> | void;
393
+ }
394
+
395
+ export interface RollbackLedger {
396
+ /** Record a teardown for a resource the moment it is created. */
397
+ register(label: string, undo: () => Promise<void> | void): void;
398
+ /** The steps, in registration order (teardown runs them REVERSED). */
399
+ steps(): readonly RollbackStep[];
400
+ }
401
+
402
+ /** A created-resources ledger for the full create flow (A3). */
403
+ export function createRollbackLedger(): RollbackLedger {
404
+ const steps: RollbackStep[] = [];
405
+ return {
406
+ register: (label, undo) => {
407
+ steps.push({ label, undo });
408
+ },
409
+ steps: () => steps,
410
+ };
411
+ }
412
+
413
+ export interface CreateRollbackOptions {
414
+ report?: (step: string, detail?: string) => void;
415
+ }
416
+
417
+ /**
418
+ * A3: run `fn`; on a throw, tear down EVERY resource the ledger recorded, in
419
+ * REVERSE order (last-created first), then rethrow with a summary of what was
420
+ * and wasn't cleaned up.
421
+ *
422
+ * Unlike {@link withLakebaseRollback} (Lakebase project only), this undoes the
423
+ * GitHub repo, the self-hosted runner, the local dir, AND the Lakebase project
424
+ * together — so a genuine provisioning fault leaves a clean "name is free" state
425
+ * instead of orphaning three of the four artifacts (the exact 4-artifact manual
426
+ * cleanup the cold-download incident caused). A step's own failure is captured +
427
+ * reported, never thrown, so one stuck teardown can't block the others; a
428
+ * "not found" teardown counts as already-gone. The caller registers a step the
429
+ * instant each resource is created, so a failure anywhere inside `fn` rolls back
430
+ * exactly what existed at that point.
431
+ */
432
+ export async function withCreateRollback<T>(
433
+ ledger: RollbackLedger,
434
+ opts: CreateRollbackOptions,
435
+ fn: () => Promise<T>,
436
+ ): Promise<T> {
437
+ try {
438
+ return await fn();
439
+ } catch (err) {
440
+ const report = opts.report ?? (() => {});
441
+ const steps = [...ledger.steps()].reverse();
442
+ const rolledBack: string[] = [];
443
+ const failed: string[] = [];
444
+ if (steps.length > 0) report(`Create failed; rolling back ${steps.length} created resource(s)...`);
445
+ for (const s of steps) {
446
+ try {
447
+ report(` rolling back: ${s.label}`);
448
+ await s.undo();
449
+ rolledBack.push(s.label);
450
+ } catch (e) {
451
+ const m = e instanceof Error ? e.message : String(e);
452
+ if (/not.?found/i.test(m)) {
453
+ rolledBack.push(s.label);
454
+ continue;
455
+ }
456
+ failed.push(`${s.label} (${m})`);
457
+ }
458
+ }
459
+ const base = err instanceof Error ? err.message : String(err);
460
+ const parts: string[] = [];
461
+ if (rolledBack.length) parts.push(`rolled back: ${rolledBack.join(", ")}`);
462
+ if (failed.length) {
463
+ parts.push(
464
+ `COULD NOT roll back: ${failed.join("; ")} , clean these up by hand before retrying the same name`,
465
+ );
466
+ }
467
+ const suffix = parts.length ? ` (${parts.join("; ")})` : "";
468
+ const wrapped = err instanceof Error ? err : new Error(base);
469
+ wrapped.message = `${base}${suffix}`;
470
+ throw wrapped;
471
+ }
472
+ }