@databricks/appkit 0.56.0 → 0.58.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 (36) hide show
  1. package/dist/appkit/package.js +1 -1
  2. package/dist/cli/commands/doctor/bundle.js +128 -0
  3. package/dist/cli/commands/doctor/bundle.js.map +1 -0
  4. package/dist/cli/commands/doctor/checks-existence.js +247 -0
  5. package/dist/cli/commands/doctor/checks-existence.js.map +1 -0
  6. package/dist/cli/commands/doctor/checks-wiring.js +35 -0
  7. package/dist/cli/commands/doctor/checks-wiring.js.map +1 -0
  8. package/dist/cli/commands/doctor/checks.js +206 -0
  9. package/dist/cli/commands/doctor/checks.js.map +1 -0
  10. package/dist/cli/commands/doctor/databricks-client.js +80 -0
  11. package/dist/cli/commands/doctor/databricks-client.js.map +1 -0
  12. package/dist/cli/commands/doctor/index.js +40 -0
  13. package/dist/cli/commands/doctor/index.js.map +1 -0
  14. package/dist/cli/commands/doctor/report.js +177 -0
  15. package/dist/cli/commands/doctor/report.js.map +1 -0
  16. package/dist/cli/commands/doctor/resolve-targets.js +86 -0
  17. package/dist/cli/commands/doctor/resolve-targets.js.map +1 -0
  18. package/dist/cli/commands/doctor/run.js +153 -0
  19. package/dist/cli/commands/doctor/run.js.map +1 -0
  20. package/dist/cli/commands/doctor/types.js +27 -0
  21. package/dist/cli/commands/doctor/types.js.map +1 -0
  22. package/dist/cli/commands/doctor/utils.js +34 -0
  23. package/dist/cli/commands/doctor/utils.js.map +1 -0
  24. package/dist/cli/index.js +2 -0
  25. package/dist/cli/index.js.map +1 -1
  26. package/dist/connectors/lakebase/index.js +2 -3
  27. package/dist/connectors/lakebase/index.js.map +1 -1
  28. package/dist/plugins/ai-search/ai-search.d.ts +21 -0
  29. package/dist/plugins/ai-search/ai-search.d.ts.map +1 -1
  30. package/dist/plugins/ai-search/ai-search.js +74 -13
  31. package/dist/plugins/ai-search/ai-search.js.map +1 -1
  32. package/dist/plugins/ai-search/defaults.js +4 -1
  33. package/dist/plugins/ai-search/defaults.js.map +1 -1
  34. package/docs/plugins/ai-search.md +10 -0
  35. package/package.json +1 -1
  36. package/sbom.cdx.json +1 -1
@@ -1,6 +1,6 @@
1
1
  //#region package.json
2
2
  var name = "@databricks/appkit";
3
- var version = "0.56.0";
3
+ var version = "0.58.0";
4
4
 
5
5
  //#endregion
6
6
  export { name, version };
@@ -0,0 +1,128 @@
1
+ import { errorMessage } from "./utils.js";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import yaml from "js-yaml";
5
+
6
+ //#region src/cli/commands/doctor/bundle.ts
7
+ /**
8
+ * Reads `databricks.yml` + `app.yaml` for two things doctor can't get from
9
+ * `appkit.plugins.json`:
10
+ *
11
+ * 1. **Provenance** — whether each binding references an existing resource
12
+ * (`${var.*}`/literal → `external`) or one this bundle creates
13
+ * (`${resources.<type>.<key>.*}` → `bundle-managed`).
14
+ * 2. **Wiring** — the binding `name` joins the three files: `app.yaml`'s
15
+ * `valueFrom: <name>` must match a `databricks.yml` binding `name`.
16
+ *
17
+ * SDK-free; pure YAML parsing. Absent files degrade to empty results.
18
+ */
19
+ const DEFAULT_BUNDLE_FILE = "databricks.yml";
20
+ const DEFAULT_APP_YAML_FILE = "app.yaml";
21
+ /** A `${resources.<type>.<key>.<field>}` reference — a bundle-created resource. */
22
+ const RESOURCES_REF = /\$\{resources\.([^.]+)\.([^.}]+)\.[^}]+\}/;
23
+ /** The typed sub-key of a binding is its single non-`name` object property. */
24
+ function bindingType(block) {
25
+ for (const [k, v] of Object.entries(block)) {
26
+ if (k === "name") continue;
27
+ if (v && typeof v === "object") return k;
28
+ }
29
+ }
30
+ /** Classifies a binding by its typed sub-key and origin: scanning its field
31
+ * values for a `${resources.*}` reference (bundle-managed) vs anything else
32
+ * (external). */
33
+ function classifyBinding(block) {
34
+ const type = bindingType(block);
35
+ const typed = type ? block[type] : void 0;
36
+ if (typed && typeof typed === "object") for (const value of Object.values(typed)) {
37
+ if (typeof value !== "string") continue;
38
+ const m = value.match(RESOURCES_REF);
39
+ if (m) return {
40
+ type,
41
+ origin: "bundle-managed",
42
+ ref: {
43
+ type: m[1],
44
+ key: m[2]
45
+ }
46
+ };
47
+ }
48
+ return {
49
+ type,
50
+ origin: "external"
51
+ };
52
+ }
53
+ /**
54
+ * Reads and parses a YAML file. Returns `null` when the file is absent (a
55
+ * legitimate "no bundle" state); throws on invalid YAML, since silently
56
+ * ignoring it would let doctor report a false all-clear.
57
+ */
58
+ function readYaml(filePath) {
59
+ let raw;
60
+ try {
61
+ raw = fs.readFileSync(filePath, "utf-8");
62
+ } catch (err) {
63
+ if (err.code === "ENOENT") return null;
64
+ throw err;
65
+ }
66
+ try {
67
+ return yaml.load(raw) ?? {};
68
+ } catch (err) {
69
+ throw new Error(`Failed to parse ${path.basename(filePath)}: ${errorMessage(err)}`);
70
+ }
71
+ }
72
+ /**
73
+ * Parses `databricks.yml` (+ `app.yaml`) into {@link BundleInfo}. Returns an
74
+ * empty-but-`present:false` result when no bundle is found, so callers can treat
75
+ * "no bundle" as "everything external" without special-casing.
76
+ */
77
+ function readBundleInfo(cwd = process.cwd(), bundleFile = DEFAULT_BUNDLE_FILE, appYamlFile = DEFAULT_APP_YAML_FILE) {
78
+ const bindings = /* @__PURE__ */ new Map();
79
+ const envToBinding = /* @__PURE__ */ new Map();
80
+ const declaredResources = /* @__PURE__ */ new Set();
81
+ const doc = readYaml(path.resolve(cwd, bundleFile));
82
+ if (!doc) return {
83
+ bindings,
84
+ envToBinding,
85
+ declaredResources,
86
+ present: false
87
+ };
88
+ for (const [type, group] of Object.entries(doc.resources ?? {})) {
89
+ if (!group || typeof group !== "object") continue;
90
+ for (const key of Object.keys(group)) declaredResources.add(`${type}.${key}`);
91
+ }
92
+ for (const app of Object.values(doc.resources?.apps ?? {})) for (const block of app.resources ?? []) {
93
+ if (!block?.name) continue;
94
+ const { type, origin, ref } = classifyBinding(block);
95
+ bindings.set(block.name, {
96
+ name: block.name,
97
+ type,
98
+ origin,
99
+ ref
100
+ });
101
+ }
102
+ const appYaml = readYaml(path.resolve(cwd, appYamlFile));
103
+ for (const entry of appYaml?.env ?? []) if (entry?.name && entry.valueFrom) envToBinding.set(entry.name, entry.valueFrom);
104
+ return {
105
+ bindings,
106
+ envToBinding,
107
+ declaredResources,
108
+ present: true
109
+ };
110
+ }
111
+ /**
112
+ * Resolves the origin for a target given its env vars, by walking
113
+ * env var → binding name → binding origin. Returns undefined when the bundle
114
+ * has nothing to say (so the target stays external by default).
115
+ */
116
+ function originForEnvVars(envVars, info) {
117
+ if (!info.present) return void 0;
118
+ for (const env of envVars) {
119
+ const bindingName = info.envToBinding.get(env);
120
+ if (!bindingName) continue;
121
+ const binding = info.bindings.get(bindingName);
122
+ if (binding) return binding.origin;
123
+ }
124
+ }
125
+
126
+ //#endregion
127
+ export { originForEnvVars, readBundleInfo };
128
+ //# sourceMappingURL=bundle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle.js","names":[],"sources":["../../../../src/cli/commands/doctor/bundle.ts"],"sourcesContent":["/**\n * Reads `databricks.yml` + `app.yaml` for two things doctor can't get from\n * `appkit.plugins.json`:\n *\n * 1. **Provenance** — whether each binding references an existing resource\n * (`${var.*}`/literal → `external`) or one this bundle creates\n * (`${resources.<type>.<key>.*}` → `bundle-managed`).\n * 2. **Wiring** — the binding `name` joins the three files: `app.yaml`'s\n * `valueFrom: <name>` must match a `databricks.yml` binding `name`.\n *\n * SDK-free; pure YAML parsing. Absent files degrade to empty results.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport yaml from \"js-yaml\";\nimport type { ResourceOrigin } from \"./types\";\nimport { errorMessage } from \"./utils\";\n\nexport const DEFAULT_BUNDLE_FILE = \"databricks.yml\";\nexport const DEFAULT_APP_YAML_FILE = \"app.yaml\";\n\n/** A `${resources.<type>.<key>.<field>}` reference — a bundle-created resource. */\nconst RESOURCES_REF = /\\$\\{resources\\.([^.]+)\\.([^.}]+)\\.[^}]+\\}/;\n\n/** One app resource binding from `resources.apps.<app>.resources[]`. */\nexport interface BundleBinding {\n /** The binding name — the join key with `app.yaml` `valueFrom`. */\n name: string;\n /** The typed sub-key that names the kind (`sql_warehouse`, `genie_space`, …). */\n type?: string;\n origin: ResourceOrigin;\n /** For bundle-managed bindings: the `<type>.<key>` it references, if parseable. */\n ref?: { type: string; key: string };\n}\n\nexport interface BundleInfo {\n /** Bindings under every app, keyed by binding name. */\n bindings: Map<string, BundleBinding>;\n /** `app.yaml` env var name → binding name (from `valueFrom`). */\n envToBinding: Map<string, string>;\n /** Declared bundle resource keys, as `\"<type>.<key>\"`, for ref-integrity. */\n declaredResources: Set<string>;\n /** True when a `databricks.yml` was found and parsed. */\n present: boolean;\n}\n\ninterface AppResourceBlock {\n name?: string;\n [k: string]: unknown;\n}\ninterface BundleAppBlock {\n resources?: AppResourceBlock[];\n}\ninterface BundleDoc {\n resources?: {\n apps?: Record<string, BundleAppBlock>;\n [otherType: string]: Record<string, unknown> | undefined;\n };\n}\ninterface AppYamlDoc {\n env?: Array<{ name?: string; valueFrom?: string }>;\n}\n\n/** The typed sub-key of a binding is its single non-`name` object property. */\nfunction bindingType(block: AppResourceBlock): string | undefined {\n for (const [k, v] of Object.entries(block)) {\n if (k === \"name\") continue;\n if (v && typeof v === \"object\") return k;\n }\n return undefined;\n}\n\n/** Classifies a binding by its typed sub-key and origin: scanning its field\n * values for a `${resources.*}` reference (bundle-managed) vs anything else\n * (external). */\nfunction classifyBinding(block: AppResourceBlock): {\n type?: string;\n origin: ResourceOrigin;\n ref?: { type: string; key: string };\n} {\n const type = bindingType(block);\n const typed = type ? block[type] : undefined;\n if (typed && typeof typed === \"object\") {\n for (const value of Object.values(typed as Record<string, unknown>)) {\n if (typeof value !== \"string\") continue;\n const m = value.match(RESOURCES_REF);\n if (m) {\n return {\n type,\n origin: \"bundle-managed\",\n ref: { type: m[1], key: m[2] },\n };\n }\n }\n }\n return { type, origin: \"external\" };\n}\n\n/**\n * Reads and parses a YAML file. Returns `null` when the file is absent (a\n * legitimate \"no bundle\" state); throws on invalid YAML, since silently\n * ignoring it would let doctor report a false all-clear.\n */\nfunction readYaml<T>(filePath: string): T | null {\n let raw: string;\n try {\n raw = fs.readFileSync(filePath, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw err;\n }\n try {\n return (yaml.load(raw) ?? {}) as T;\n } catch (err) {\n throw new Error(\n `Failed to parse ${path.basename(filePath)}: ${errorMessage(err)}`,\n );\n }\n}\n\n/**\n * Parses `databricks.yml` (+ `app.yaml`) into {@link BundleInfo}. Returns an\n * empty-but-`present:false` result when no bundle is found, so callers can treat\n * \"no bundle\" as \"everything external\" without special-casing.\n */\nexport function readBundleInfo(\n cwd: string = process.cwd(),\n bundleFile: string = DEFAULT_BUNDLE_FILE,\n appYamlFile: string = DEFAULT_APP_YAML_FILE,\n): BundleInfo {\n const bindings = new Map<string, BundleBinding>();\n const envToBinding = new Map<string, string>();\n const declaredResources = new Set<string>();\n\n const doc = readYaml<BundleDoc>(path.resolve(cwd, bundleFile));\n if (!doc) {\n return { bindings, envToBinding, declaredResources, present: false };\n }\n\n for (const [type, group] of Object.entries(doc.resources ?? {})) {\n if (!group || typeof group !== \"object\") continue;\n for (const key of Object.keys(group))\n declaredResources.add(`${type}.${key}`);\n }\n\n for (const app of Object.values(doc.resources?.apps ?? {})) {\n for (const block of app.resources ?? []) {\n if (!block?.name) continue;\n const { type, origin, ref } = classifyBinding(block);\n bindings.set(block.name, {\n name: block.name,\n type,\n origin,\n ref,\n });\n }\n }\n\n const appYaml = readYaml<AppYamlDoc>(path.resolve(cwd, appYamlFile));\n for (const entry of appYaml?.env ?? []) {\n if (entry?.name && entry.valueFrom) {\n envToBinding.set(entry.name, entry.valueFrom);\n }\n }\n\n return { bindings, envToBinding, declaredResources, present: true };\n}\n\n/**\n * Resolves the origin for a target given its env vars, by walking\n * env var → binding name → binding origin. Returns undefined when the bundle\n * has nothing to say (so the target stays external by default).\n */\nexport function originForEnvVars(\n envVars: string[],\n info: BundleInfo,\n): ResourceOrigin | undefined {\n if (!info.present) return undefined;\n for (const env of envVars) {\n const bindingName = info.envToBinding.get(env);\n if (!bindingName) continue;\n const binding = info.bindings.get(bindingName);\n if (binding) return binding.origin;\n }\n return undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAmBA,MAAa,sBAAsB;AACnC,MAAa,wBAAwB;;AAGrC,MAAM,gBAAgB;;AA0CtB,SAAS,YAAY,OAA6C;AAChE,MAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,MAAM,EAAE;AAC1C,MAAI,MAAM,OAAQ;AAClB,MAAI,KAAK,OAAO,MAAM,SAAU,QAAO;;;;;;AAQ3C,SAAS,gBAAgB,OAIvB;CACA,MAAM,OAAO,YAAY,MAAM;CAC/B,MAAM,QAAQ,OAAO,MAAM,QAAQ;AACnC,KAAI,SAAS,OAAO,UAAU,SAC5B,MAAK,MAAM,SAAS,OAAO,OAAO,MAAiC,EAAE;AACnE,MAAI,OAAO,UAAU,SAAU;EAC/B,MAAM,IAAI,MAAM,MAAM,cAAc;AACpC,MAAI,EACF,QAAO;GACL;GACA,QAAQ;GACR,KAAK;IAAE,MAAM,EAAE;IAAI,KAAK,EAAE;IAAI;GAC/B;;AAIP,QAAO;EAAE;EAAM,QAAQ;EAAY;;;;;;;AAQrC,SAAS,SAAY,UAA4B;CAC/C,IAAI;AACJ,KAAI;AACF,QAAM,GAAG,aAAa,UAAU,QAAQ;UACjC,KAAK;AACZ,MAAK,IAA8B,SAAS,SAAU,QAAO;AAC7D,QAAM;;AAER,KAAI;AACF,SAAQ,KAAK,KAAK,IAAI,IAAI,EAAE;UACrB,KAAK;AACZ,QAAM,IAAI,MACR,mBAAmB,KAAK,SAAS,SAAS,CAAC,IAAI,aAAa,IAAI,GACjE;;;;;;;;AASL,SAAgB,eACd,MAAc,QAAQ,KAAK,EAC3B,aAAqB,qBACrB,cAAsB,uBACV;CACZ,MAAM,2BAAW,IAAI,KAA4B;CACjD,MAAM,+BAAe,IAAI,KAAqB;CAC9C,MAAM,oCAAoB,IAAI,KAAa;CAE3C,MAAM,MAAM,SAAoB,KAAK,QAAQ,KAAK,WAAW,CAAC;AAC9D,KAAI,CAAC,IACH,QAAO;EAAE;EAAU;EAAc;EAAmB,SAAS;EAAO;AAGtE,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,IAAI,aAAa,EAAE,CAAC,EAAE;AAC/D,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU;AACzC,OAAK,MAAM,OAAO,OAAO,KAAK,MAAM,CAClC,mBAAkB,IAAI,GAAG,KAAK,GAAG,MAAM;;AAG3C,MAAK,MAAM,OAAO,OAAO,OAAO,IAAI,WAAW,QAAQ,EAAE,CAAC,CACxD,MAAK,MAAM,SAAS,IAAI,aAAa,EAAE,EAAE;AACvC,MAAI,CAAC,OAAO,KAAM;EAClB,MAAM,EAAE,MAAM,QAAQ,QAAQ,gBAAgB,MAAM;AACpD,WAAS,IAAI,MAAM,MAAM;GACvB,MAAM,MAAM;GACZ;GACA;GACA;GACD,CAAC;;CAIN,MAAM,UAAU,SAAqB,KAAK,QAAQ,KAAK,YAAY,CAAC;AACpE,MAAK,MAAM,SAAS,SAAS,OAAO,EAAE,CACpC,KAAI,OAAO,QAAQ,MAAM,UACvB,cAAa,IAAI,MAAM,MAAM,MAAM,UAAU;AAIjD,QAAO;EAAE;EAAU;EAAc;EAAmB,SAAS;EAAM;;;;;;;AAQrE,SAAgB,iBACd,SACA,MAC4B;AAC5B,KAAI,CAAC,KAAK,QAAS,QAAO;AAC1B,MAAK,MAAM,OAAO,SAAS;EACzB,MAAM,cAAc,KAAK,aAAa,IAAI,IAAI;AAC9C,MAAI,CAAC,YAAa;EAClB,MAAM,UAAU,KAAK,SAAS,IAAI,YAAY;AAC9C,MAAI,QAAS,QAAO,QAAQ"}
@@ -0,0 +1,247 @@
1
+ import { errorMessage } from "./utils.js";
2
+ import { AppkitNotInstalledError, getLakebasePool } from "./databricks-client.js";
3
+
4
+ //#region src/cli/commands/doctor/checks-existence.ts
5
+ /**
6
+ * Layer: existence — per-resource-type probes that prove a declared resource
7
+ * exists and is reachable via the cheapest read the SDK offers.
8
+ *
9
+ * The client is typed structurally (not via the SDK) so `shared` stays SDK-free.
10
+ */
11
+ /** A passing existence check. Shared, never mutated by callers. */
12
+ const EXISTENCE_OK = {
13
+ layer: "existence",
14
+ status: "ok"
15
+ };
16
+ function statusCodeOf(err) {
17
+ if (err && typeof err === "object" && "statusCode" in err) {
18
+ const code = err.statusCode;
19
+ if (typeof code === "number") return code;
20
+ }
21
+ }
22
+ function errorCodeOf(err) {
23
+ if (err && typeof err === "object" && "errorCode" in err) {
24
+ const code = err.errorCode;
25
+ if (typeof code === "string" && code.length > 0) return code;
26
+ }
27
+ }
28
+ function cleanMessage(err) {
29
+ const raw = errorMessage(err);
30
+ const m = raw.match(/"message"\s*:\s*"([^"]+)"/);
31
+ return m ? m[1] : raw;
32
+ }
33
+ /** The resource's configured identifier (id / name / path / host), for short
34
+ * messages. */
35
+ function displayId(target) {
36
+ return field(target, "id", "name", "path", "index_name", "indexName", "host");
37
+ }
38
+ function classifyError(err, target) {
39
+ const status = statusCodeOf(err);
40
+ const errorCode = errorCodeOf(err);
41
+ const message = cleanMessage(err);
42
+ const id = displayId(target);
43
+ const quoted = id ? `"${id}"` : null;
44
+ if (status === 404 || errorCode === "RESOURCE_DOES_NOT_EXIST") return {
45
+ layer: "existence",
46
+ status: "error",
47
+ code: "NOT_FOUND",
48
+ detail: quoted ? `${quoted} not found` : "not found"
49
+ };
50
+ if (status === 400 || errorCode === "INVALID_PARAMETER_VALUE") return {
51
+ layer: "existence",
52
+ status: "error",
53
+ code: "INVALID_VALUE",
54
+ detail: quoted ? `${quoted} is not a valid id/name` : `invalid id/name: ${message}`
55
+ };
56
+ if (status === 403 || errorCode === "PERMISSION_DENIED") return {
57
+ layer: "existence",
58
+ status: "error",
59
+ code: "ACCESS_DENIED",
60
+ detail: quoted ? `${quoted} not found, or you don't have access to it` : "not found, or you don't have access to it"
61
+ };
62
+ return {
63
+ layer: "existence",
64
+ status: "error",
65
+ code: "PROBE_FAILED",
66
+ detail: quoted ? `read failed for ${quoted}: ${message}` : `read failed: ${message}`
67
+ };
68
+ }
69
+ /** Normalizes a field key so camelCase (`indexName`) and snake_case
70
+ * (`index_name`) spellings match. */
71
+ function normalizeKey(key) {
72
+ return key.toLowerCase().replace(/[_-]/g, "");
73
+ }
74
+ /** Looks up a resolved field value by any of the accepted key spellings. */
75
+ function field(target, ...names) {
76
+ const wanted = new Set(names.map(normalizeKey));
77
+ for (const [key, value] of Object.entries(target.fieldValues)) if (wanted.has(normalizeKey(key)) && value.length > 0) return value;
78
+ return null;
79
+ }
80
+ function missingField(fieldName) {
81
+ return {
82
+ layer: "existence",
83
+ status: "skipped",
84
+ code: "MISSING_FIELD",
85
+ detail: `no ${fieldName} configured — can't probe`
86
+ };
87
+ }
88
+ const probeWarehouse = async (client, target) => {
89
+ const id = field(target, "id");
90
+ if (!id) return missingField("id");
91
+ try {
92
+ const state = (await client.warehouses.get({ id })).state;
93
+ if (state && state !== "RUNNING") return {
94
+ layer: "existence",
95
+ status: "warn",
96
+ code: "WAREHOUSE_NOT_RUNNING",
97
+ detail: `warehouse exists but is ${state} (will cold-start on first query)`
98
+ };
99
+ return EXISTENCE_OK;
100
+ } catch (err) {
101
+ return classifyError(err, target);
102
+ }
103
+ };
104
+ const probeServing = async (client, target) => {
105
+ const name = field(target, "name");
106
+ const idOnly = name === null ? field(target, "id") : null;
107
+ const value = name ?? idOnly;
108
+ if (!value) return missingField("name");
109
+ try {
110
+ await client.servingEndpoints.get({ name: value });
111
+ return EXISTENCE_OK;
112
+ } catch (err) {
113
+ const result = classifyError(err, target);
114
+ if (idOnly && result.status === "error") result.hint = "Serving endpoints are looked up by name, but this resource is configured by id. Set DATABRICKS_SERVING_ENDPOINT_NAME to the endpoint's name.";
115
+ return result;
116
+ }
117
+ };
118
+ const probeGenie = async (client, target) => {
119
+ const spaceId = field(target, "id");
120
+ if (!spaceId) return missingField("id");
121
+ try {
122
+ await client.genie.getSpace({ space_id: spaceId });
123
+ return EXISTENCE_OK;
124
+ } catch (err) {
125
+ return classifyError(err, target);
126
+ }
127
+ };
128
+ const probeJob = async (client, target) => {
129
+ const raw = field(target, "id");
130
+ if (!raw) return missingField("id");
131
+ const id = raw.trim();
132
+ if (!/^\d+$/.test(id)) return {
133
+ layer: "existence",
134
+ status: "error",
135
+ code: "INVALID_ID",
136
+ detail: `"${raw}" is not a valid job id (expected an integer)`
137
+ };
138
+ try {
139
+ await client.jobs.get({ job_id: id });
140
+ return EXISTENCE_OK;
141
+ } catch (err) {
142
+ return classifyError(err, target);
143
+ }
144
+ };
145
+ const probeVolume = async (client, target) => {
146
+ const raw = field(target, "path", "name");
147
+ if (!raw) return missingField("path");
148
+ const name = toThreeLevelVolumeName(raw);
149
+ if (!name) return {
150
+ layer: "existence",
151
+ status: "error",
152
+ code: "INVALID_NAME",
153
+ detail: `"${raw}" is not a valid volume path (expected /Volumes/catalog/schema/volume or catalog.schema.volume)`
154
+ };
155
+ try {
156
+ await client.volumes.read({ name });
157
+ return EXISTENCE_OK;
158
+ } catch (err) {
159
+ return classifyError(err, target);
160
+ }
161
+ };
162
+ const probeVectorIndex = async (client, target) => {
163
+ const name = field(target, "indexName", "index_name", "name");
164
+ if (!name) return missingField("indexName");
165
+ try {
166
+ await client.vectorSearchIndexes.getIndex({ index_name: name });
167
+ return EXISTENCE_OK;
168
+ } catch (err) {
169
+ return classifyError(err, target);
170
+ }
171
+ };
172
+ const probeFunction = async (client, target) => {
173
+ const name = field(target, "name");
174
+ if (!name) return missingField("name");
175
+ try {
176
+ await client.functions.get({ name });
177
+ return EXISTENCE_OK;
178
+ } catch (err) {
179
+ return classifyError(err, target);
180
+ }
181
+ };
182
+ function lakebaseAuthHint(message) {
183
+ if (/password authentication failed/i.test(message)) return "Lakebase uses an OAuth token as the password, so this usually means the PGUSER/role doesn't match your identity. Check PGUSER is your exact login.";
184
+ }
185
+ const probePostgres = async (client, target) => {
186
+ if (!field(target, "endpointPath") && !field(target, "host")) return missingField("host/endpoint");
187
+ let pool = null;
188
+ try {
189
+ pool = await getLakebasePool(client);
190
+ await pool.query("SELECT 1");
191
+ return EXISTENCE_OK;
192
+ } catch (err) {
193
+ if (err instanceof AppkitNotInstalledError) return {
194
+ layer: "existence",
195
+ status: "skipped",
196
+ code: "APPKIT_NOT_INSTALLED",
197
+ detail: err.message
198
+ };
199
+ const message = cleanMessage(err);
200
+ return {
201
+ layer: "existence",
202
+ status: "error",
203
+ code: "CONNECTION_FAILED",
204
+ detail: `could not connect to Lakebase Postgres: ${message}`,
205
+ hint: lakebaseAuthHint(message)
206
+ };
207
+ } finally {
208
+ if (pool) try {
209
+ await pool.end();
210
+ } catch {}
211
+ }
212
+ };
213
+ /**
214
+ * Normalizes a volume reference to the SDK's 3-level `catalog.schema.volume`
215
+ * name, accepting either an already-dotted name or a `/Volumes/c/s/v/...` path.
216
+ */
217
+ function toThreeLevelVolumeName(raw) {
218
+ const trimmed = raw.trim();
219
+ if (/^[^./\s]+\.[^./\s]+\.[^./\s]+$/.test(trimmed)) return trimmed;
220
+ const m = trimmed.match(/^\/Volumes\/([^/]+)\/([^/]+)\/([^/]+)/);
221
+ if (m) return `${m[1]}.${m[2]}.${m[3]}`;
222
+ return null;
223
+ }
224
+ const PROBES = {
225
+ sql_warehouse: probeWarehouse,
226
+ serving_endpoint: probeServing,
227
+ genie_space: probeGenie,
228
+ job: probeJob,
229
+ volume: probeVolume,
230
+ vector_search_index: probeVectorIndex,
231
+ uc_function: probeFunction,
232
+ postgres: probePostgres
233
+ };
234
+ async function runExistenceProbe(client, target) {
235
+ const probe = PROBES[target.type];
236
+ if (!probe) return {
237
+ layer: "existence",
238
+ status: "skipped",
239
+ code: "NOT_IMPLEMENTED",
240
+ detail: `existence check not implemented for ${target.type}`
241
+ };
242
+ return probe(client, target);
243
+ }
244
+
245
+ //#endregion
246
+ export { runExistenceProbe };
247
+ //# sourceMappingURL=checks-existence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checks-existence.js","names":[],"sources":["../../../../src/cli/commands/doctor/checks-existence.ts"],"sourcesContent":["/**\n * Layer: existence — per-resource-type probes that prove a declared resource\n * exists and is reachable via the cheapest read the SDK offers.\n *\n * The client is typed structurally (not via the SDK) so `shared` stays SDK-free.\n */\n\nimport {\n AppkitNotInstalledError,\n getLakebasePool,\n type LakebasePoolHandle,\n} from \"./databricks-client\";\nimport type { LayerResult, ResourceTarget } from \"./types\";\nimport { errorMessage } from \"./utils\";\n\n/** A passing existence check. Shared, never mutated by callers. */\nconst EXISTENCE_OK: LayerResult = { layer: \"existence\", status: \"ok\" };\n\ninterface DoctorWorkspaceClient {\n warehouses: {\n get: (r: { id: string }) => Promise<{ state?: string }>;\n };\n servingEndpoints: {\n get: (r: { name: string }) => Promise<unknown>;\n };\n genie: {\n getSpace: (r: { space_id: string }) => Promise<unknown>;\n };\n jobs: {\n get: (r: { job_id: number }) => Promise<unknown>;\n };\n volumes: {\n read: (r: { name: string }) => Promise<unknown>;\n };\n vectorSearchIndexes: {\n getIndex: (r: { index_name: string }) => Promise<unknown>;\n };\n functions: {\n get: (r: { name: string }) => Promise<unknown>;\n };\n}\n\ntype ExistenceProbe = (\n client: DoctorWorkspaceClient,\n target: ResourceTarget,\n) => Promise<LayerResult>;\n\n// Read statusCode/errorCode off the SDK's ApiError structurally.\nfunction statusCodeOf(err: unknown): number | undefined {\n if (err && typeof err === \"object\" && \"statusCode\" in err) {\n const code = (err as { statusCode?: unknown }).statusCode;\n if (typeof code === \"number\") return code;\n }\n return undefined;\n}\n\nfunction errorCodeOf(err: unknown): string | undefined {\n if (err && typeof err === \"object\" && \"errorCode\" in err) {\n const code = (err as { errorCode?: unknown }).errorCode;\n if (typeof code === \"string\" && code.length > 0) return code;\n }\n return undefined;\n}\n\n// The SDK message often embeds a JSON blob; pull the inner `message` out so\n// doctor prints one clean line, not a dump.\nfunction cleanMessage(err: unknown): string {\n const raw = errorMessage(err);\n const m = raw.match(/\"message\"\\s*:\\s*\"([^\"]+)\"/);\n return m ? m[1] : raw;\n}\n\n/** The resource's configured identifier (id / name / path / host), for short\n * messages. */\nfunction displayId(target: ResourceTarget): string | null {\n return field(target, \"id\", \"name\", \"path\", \"index_name\", \"indexName\", \"host\");\n}\n\nfunction classifyError(err: unknown, target: ResourceTarget): LayerResult {\n const status = statusCodeOf(err);\n const errorCode = errorCodeOf(err);\n const message = cleanMessage(err);\n const id = displayId(target);\n const quoted = id ? `\"${id}\"` : null;\n\n if (status === 404 || errorCode === \"RESOURCE_DOES_NOT_EXIST\") {\n return {\n layer: \"existence\",\n status: \"error\",\n code: \"NOT_FOUND\",\n detail: quoted ? `${quoted} not found` : \"not found\",\n };\n }\n // A malformed id/name often comes back as 400 rather than 404.\n if (status === 400 || errorCode === \"INVALID_PARAMETER_VALUE\") {\n return {\n layer: \"existence\",\n status: \"error\",\n code: \"INVALID_VALUE\",\n detail: quoted\n ? `${quoted} is not a valid id/name`\n : `invalid id/name: ${message}`,\n };\n }\n // A 403 is genuinely ambiguous: several APIs (jobs, warehouses) return it for a\n // resource that doesn't exist as well as for one you can't read, so claiming\n // \"no permission\" outright would misdiagnose a wrong id. Say both.\n if (status === 403 || errorCode === \"PERMISSION_DENIED\") {\n return {\n layer: \"existence\",\n status: \"error\",\n code: \"ACCESS_DENIED\",\n detail: quoted\n ? `${quoted} not found, or you don't have access to it`\n : \"not found, or you don't have access to it\",\n };\n }\n return {\n layer: \"existence\",\n status: \"error\",\n code: \"PROBE_FAILED\",\n detail: quoted\n ? `read failed for ${quoted}: ${message}`\n : `read failed: ${message}`,\n };\n}\n\n/** Normalizes a field key so camelCase (`indexName`) and snake_case\n * (`index_name`) spellings match. */\nfunction normalizeKey(key: string): string {\n return key.toLowerCase().replace(/[_-]/g, \"\");\n}\n\n/** Looks up a resolved field value by any of the accepted key spellings. */\nfunction field(target: ResourceTarget, ...names: string[]): string | null {\n const wanted = new Set(names.map(normalizeKey));\n for (const [key, value] of Object.entries(target.fieldValues)) {\n if (wanted.has(normalizeKey(key)) && value.length > 0) return value;\n }\n return null;\n}\n\nfunction missingField(fieldName: string): LayerResult {\n return {\n layer: \"existence\",\n status: \"skipped\",\n code: \"MISSING_FIELD\",\n detail: `no ${fieldName} configured — can't probe`,\n };\n}\n\nconst probeWarehouse: ExistenceProbe = async (client, target) => {\n const id = field(target, \"id\");\n if (!id) return missingField(\"id\");\n try {\n const wh = await client.warehouses.get({ id });\n const state = wh.state;\n if (state && state !== \"RUNNING\") {\n return {\n layer: \"existence\",\n status: \"warn\",\n code: \"WAREHOUSE_NOT_RUNNING\",\n detail: `warehouse exists but is ${state} (will cold-start on first query)`,\n };\n }\n return EXISTENCE_OK;\n } catch (err) {\n return classifyError(err, target);\n }\n};\n\nconst probeServing: ExistenceProbe = async (client, target) => {\n const name = field(target, \"name\");\n // The API only accepts a name; if configured by id, probe with it anyway and\n // flag the likely cause on failure.\n const idOnly = name === null ? field(target, \"id\") : null;\n const value = name ?? idOnly;\n if (!value) return missingField(\"name\");\n try {\n await client.servingEndpoints.get({ name: value });\n return EXISTENCE_OK;\n } catch (err) {\n const result = classifyError(err, target);\n if (idOnly && result.status === \"error\") {\n result.hint =\n \"Serving endpoints are looked up by name, but this resource is configured by id. Set DATABRICKS_SERVING_ENDPOINT_NAME to the endpoint's name.\";\n }\n return result;\n }\n};\n\nconst probeGenie: ExistenceProbe = async (client, target) => {\n const spaceId = field(target, \"id\");\n if (!spaceId) return missingField(\"id\");\n try {\n await client.genie.getSpace({ space_id: spaceId });\n return EXISTENCE_OK;\n } catch (err) {\n return classifyError(err, target);\n }\n};\n\nconst probeJob: ExistenceProbe = async (client, target) => {\n const raw = field(target, \"id\");\n if (!raw) return missingField(\"id\");\n const id = raw.trim();\n // Validate the digits directly rather than via Number(): job ids are int64, so\n // past 2^53 Number() silently rounds (and Number.isInteger still passes),\n // which would probe a *different* job and report a false not-found. A string\n // check also rejects \"1e3\" and \"0x10\", both of which Number() would accept.\n if (!/^\\d+$/.test(id)) {\n return {\n layer: \"existence\",\n status: \"error\",\n code: \"INVALID_ID\",\n detail: `\"${raw}\" is not a valid job id (expected an integer)`,\n };\n }\n try {\n // Pass the digits through unconverted to preserve int64 precision; the SDK\n // serialises them straight into the request, so a string works even though\n // the type says number.\n await client.jobs.get({ job_id: id as unknown as number });\n return EXISTENCE_OK;\n } catch (err) {\n return classifyError(err, target);\n }\n};\n\nconst probeVolume: ExistenceProbe = async (client, target) => {\n // The read API wants the 3-level name (catalog.schema.volume), but the env\n // value is usually a /Volumes/... path.\n const raw = field(target, \"path\", \"name\");\n if (!raw) return missingField(\"path\");\n const name = toThreeLevelVolumeName(raw);\n if (!name) {\n return {\n layer: \"existence\",\n status: \"error\",\n code: \"INVALID_NAME\",\n detail: `\"${raw}\" is not a valid volume path (expected /Volumes/catalog/schema/volume or catalog.schema.volume)`,\n };\n }\n try {\n await client.volumes.read({ name });\n return EXISTENCE_OK;\n } catch (err) {\n return classifyError(err, target);\n }\n};\n\nconst probeVectorIndex: ExistenceProbe = async (client, target) => {\n const name = field(target, \"indexName\", \"index_name\", \"name\");\n if (!name) return missingField(\"indexName\");\n try {\n await client.vectorSearchIndexes.getIndex({ index_name: name });\n return EXISTENCE_OK;\n } catch (err) {\n return classifyError(err, target);\n }\n};\n\nconst probeFunction: ExistenceProbe = async (client, target) => {\n const name = field(target, \"name\");\n if (!name) return missingField(\"name\");\n try {\n await client.functions.get({ name });\n return EXISTENCE_OK;\n } catch (err) {\n return classifyError(err, target);\n }\n};\n\nfunction lakebaseAuthHint(message: string): string | undefined {\n if (/password authentication failed/i.test(message)) {\n return (\n \"Lakebase uses an OAuth token as the password, so this usually means the\" +\n \" PGUSER/role doesn't match your identity. Check PGUSER is your exact\" +\n \" login.\"\n );\n }\n return undefined;\n}\n\n// Lakebase has no cheap control-plane `.get()`, so existence is proven by a\n// real connection + `SELECT 1`.\nconst probePostgres: ExistenceProbe = async (client, target) => {\n if (!field(target, \"endpointPath\") && !field(target, \"host\")) {\n return missingField(\"host/endpoint\");\n }\n\n let pool: LakebasePoolHandle | null = null;\n try {\n pool = await getLakebasePool(client);\n await pool.query(\"SELECT 1\");\n return EXISTENCE_OK;\n } catch (err) {\n if (err instanceof AppkitNotInstalledError) {\n return {\n layer: \"existence\",\n status: \"skipped\",\n code: \"APPKIT_NOT_INSTALLED\",\n detail: err.message,\n };\n }\n const message = cleanMessage(err);\n return {\n layer: \"existence\",\n status: \"error\",\n code: \"CONNECTION_FAILED\",\n detail: `could not connect to Lakebase Postgres: ${message}`,\n hint: lakebaseAuthHint(message),\n };\n } finally {\n if (pool) {\n try {\n await pool.end();\n } catch {\n // best-effort close\n }\n }\n }\n};\n\n/**\n * Normalizes a volume reference to the SDK's 3-level `catalog.schema.volume`\n * name, accepting either an already-dotted name or a `/Volumes/c/s/v/...` path.\n */\nexport function toThreeLevelVolumeName(raw: string): string | null {\n const trimmed = raw.trim();\n if (/^[^./\\s]+\\.[^./\\s]+\\.[^./\\s]+$/.test(trimmed)) return trimmed;\n\n const m = trimmed.match(/^\\/Volumes\\/([^/]+)\\/([^/]+)\\/([^/]+)/);\n if (m) return `${m[1]}.${m[2]}.${m[3]}`;\n\n return null;\n}\n\n// Unlisted types fall through to NOT_IMPLEMENTED in runExistenceProbe.\nconst PROBES: Record<string, ExistenceProbe> = {\n sql_warehouse: probeWarehouse,\n serving_endpoint: probeServing,\n genie_space: probeGenie,\n job: probeJob,\n volume: probeVolume,\n vector_search_index: probeVectorIndex,\n uc_function: probeFunction,\n postgres: probePostgres,\n};\n\nexport async function runExistenceProbe(\n client: unknown,\n target: ResourceTarget,\n): Promise<LayerResult> {\n const probe = PROBES[target.type];\n if (!probe) {\n return {\n layer: \"existence\",\n status: \"skipped\",\n code: \"NOT_IMPLEMENTED\",\n detail: `existence check not implemented for ${target.type}`,\n };\n }\n return probe(client as DoctorWorkspaceClient, target);\n}\n"],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ //#region src/cli/commands/doctor/checks-wiring.ts
2
+ function checkWiring(info, targets) {
3
+ if (!info.present) return [];
4
+ const findings = [];
5
+ for (const [envVar, bindingName] of info.envToBinding) if (!info.bindings.has(bindingName)) findings.push({
6
+ status: "error",
7
+ code: "VALUEFROM_UNBOUND",
8
+ label: envVar,
9
+ detail: `app.yaml binds it to "${bindingName}", which databricks.yml doesn't declare`,
10
+ hint: `Add a resource named "${bindingName}" under resources.apps.<app>.resources, or fix the valueFrom to match an existing binding.`
11
+ });
12
+ for (const binding of info.bindings.values()) {
13
+ if (binding.origin !== "bundle-managed" || !binding.ref) continue;
14
+ const ref = `${binding.ref.type}.${binding.ref.key}`;
15
+ if (!info.declaredResources.has(ref)) findings.push({
16
+ status: "error",
17
+ code: "BUNDLE_REF_MISSING",
18
+ label: binding.name,
19
+ detail: `references \${resources.${ref}.*}, but no resources.${binding.ref.type}.${binding.ref.key} is declared in the bundle`,
20
+ hint: `Declare resources.${binding.ref.type}.${binding.ref.key} in databricks.yml, or point the binding at an existing resource.`
21
+ });
22
+ }
23
+ for (const target of targets) for (const envVar of target.envVars) if (!info.envToBinding.has(envVar)) findings.push({
24
+ status: target.required ? "error" : "warn",
25
+ code: "ENV_UNWIRED",
26
+ label: envVar,
27
+ detail: `has no app.yaml entry — it won't be set in the environment of the deployed app${target.required ? "" : " (optional)"}`,
28
+ hint: `Add to app.yaml: \`{ name: ${envVar}, valueFrom: <binding> }\`, and declare that binding in databricks.yml.`
29
+ });
30
+ return findings;
31
+ }
32
+
33
+ //#endregion
34
+ export { checkWiring };
35
+ //# sourceMappingURL=checks-wiring.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checks-wiring.js","names":[],"sources":["../../../../src/cli/commands/doctor/checks-wiring.ts"],"sourcesContent":["/**\n * Layer: wiring (offline, deploy-declaration). Validates the three-file join\n * that `databricks bundle validate` can't see, because it spans `app.yaml` and\n * the AppKit plugin layer:\n *\n * - every `app.yaml` `valueFrom: <name>` must match a `databricks.yml` binding\n * `name` — otherwise the platform has nothing to inject and the env var the\n * plugin reads stays empty at runtime;\n * - every bundle-managed `${resources.<type>.<key>.*}` binding must reference a\n * resource actually declared in the bundle — otherwise deploy fails.\n *\n * Runs regardless of auth; these are declaration problems, not connectivity.\n */\n\nimport type { BundleInfo } from \"./bundle\";\nimport type { ResourceTarget, WiringFinding } from \"./types\";\n\nexport function checkWiring(\n info: BundleInfo,\n targets: ResourceTarget[],\n): WiringFinding[] {\n if (!info.present) return [];\n\n const findings: WiringFinding[] = [];\n\n // 1. Each app.yaml valueFrom must resolve to a real binding.\n for (const [envVar, bindingName] of info.envToBinding) {\n if (!info.bindings.has(bindingName)) {\n findings.push({\n status: \"error\",\n code: \"VALUEFROM_UNBOUND\",\n label: envVar,\n detail: `app.yaml binds it to \"${bindingName}\", which databricks.yml doesn't declare`,\n hint: `Add a resource named \"${bindingName}\" under resources.apps.<app>.resources, or fix the valueFrom to match an existing binding.`,\n });\n }\n }\n\n // 2. Each bundle-managed binding must reference a declared bundle resource.\n for (const binding of info.bindings.values()) {\n if (binding.origin !== \"bundle-managed\" || !binding.ref) continue;\n const ref = `${binding.ref.type}.${binding.ref.key}`;\n if (!info.declaredResources.has(ref)) {\n findings.push({\n status: \"error\",\n code: \"BUNDLE_REF_MISSING\",\n label: binding.name,\n detail: `references \\${resources.${ref}.*}, but no resources.${binding.ref.type}.${binding.ref.key} is declared in the bundle`,\n hint: `Declare resources.${binding.ref.type}.${binding.ref.key} in databricks.yml, or point the binding at an existing resource.`,\n });\n }\n }\n\n // 3. A used plugin's env var with no app.yaml entry: set locally via .env, but\n // .env isn't uploaded and the platform injects only what app.yaml maps, so the\n // var is absent from the deployed app's environment — the headline \"works\n // locally, breaks on deploy\" case. For a *required* resource this guarantees a\n // broken deploy, so it's an error that gates the exit code; for an optional one\n // it's a warning.\n for (const target of targets) {\n for (const envVar of target.envVars) {\n if (!info.envToBinding.has(envVar)) {\n findings.push({\n status: target.required ? \"error\" : \"warn\",\n code: \"ENV_UNWIRED\",\n label: envVar,\n detail: `has no app.yaml entry — it won't be set in the environment of the deployed app${\n target.required ? \"\" : \" (optional)\"\n }`,\n hint: `Add to app.yaml: \\`{ name: ${envVar}, valueFrom: <binding> }\\`, and declare that binding in databricks.yml.`,\n });\n }\n }\n }\n\n return findings;\n}\n"],"mappings":";AAiBA,SAAgB,YACd,MACA,SACiB;AACjB,KAAI,CAAC,KAAK,QAAS,QAAO,EAAE;CAE5B,MAAM,WAA4B,EAAE;AAGpC,MAAK,MAAM,CAAC,QAAQ,gBAAgB,KAAK,aACvC,KAAI,CAAC,KAAK,SAAS,IAAI,YAAY,CACjC,UAAS,KAAK;EACZ,QAAQ;EACR,MAAM;EACN,OAAO;EACP,QAAQ,yBAAyB,YAAY;EAC7C,MAAM,yBAAyB,YAAY;EAC5C,CAAC;AAKN,MAAK,MAAM,WAAW,KAAK,SAAS,QAAQ,EAAE;AAC5C,MAAI,QAAQ,WAAW,oBAAoB,CAAC,QAAQ,IAAK;EACzD,MAAM,MAAM,GAAG,QAAQ,IAAI,KAAK,GAAG,QAAQ,IAAI;AAC/C,MAAI,CAAC,KAAK,kBAAkB,IAAI,IAAI,CAClC,UAAS,KAAK;GACZ,QAAQ;GACR,MAAM;GACN,OAAO,QAAQ;GACf,QAAQ,2BAA2B,IAAI,wBAAwB,QAAQ,IAAI,KAAK,GAAG,QAAQ,IAAI,IAAI;GACnG,MAAM,qBAAqB,QAAQ,IAAI,KAAK,GAAG,QAAQ,IAAI,IAAI;GAChE,CAAC;;AAUN,MAAK,MAAM,UAAU,QACnB,MAAK,MAAM,UAAU,OAAO,QAC1B,KAAI,CAAC,KAAK,aAAa,IAAI,OAAO,CAChC,UAAS,KAAK;EACZ,QAAQ,OAAO,WAAW,UAAU;EACpC,MAAM;EACN,OAAO;EACP,QAAQ,iFACN,OAAO,WAAW,KAAK;EAEzB,MAAM,8BAA8B,OAAO;EAC5C,CAAC;AAKR,QAAO"}