@dbx-tools/appkit-generator 0.0.0-stage → 0.9.136

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 (60) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +181 -2
  3. package/index.ts +44 -0
  4. package/lib/index.d.ts +40 -0
  5. package/lib/index.js +30 -0
  6. package/lib/src/assets.d.ts +33 -0
  7. package/lib/src/assets.js +224 -0
  8. package/lib/src/config.d.ts +75 -0
  9. package/lib/src/config.js +31 -0
  10. package/lib/src/dashboard.d.ts +30 -0
  11. package/lib/src/dashboard.js +257 -0
  12. package/lib/src/data.d.ts +17 -0
  13. package/lib/src/data.js +114 -0
  14. package/lib/src/defaults.d.ts +39 -0
  15. package/lib/src/defaults.js +36 -0
  16. package/lib/src/generate.d.ts +51 -0
  17. package/lib/src/generate.js +163 -0
  18. package/lib/src/genie.d.ts +51 -0
  19. package/lib/src/genie.js +84 -0
  20. package/lib/src/kpi.d.ts +22 -0
  21. package/lib/src/kpi.js +74 -0
  22. package/lib/src/manifest.json +72 -0
  23. package/lib/src/namespace.d.ts +36 -0
  24. package/lib/src/namespace.js +119 -0
  25. package/lib/src/operations.d.ts +42 -0
  26. package/lib/src/operations.js +96 -0
  27. package/lib/src/palette.d.ts +10 -0
  28. package/lib/src/palette.js +69 -0
  29. package/lib/src/plan.d.ts +21 -0
  30. package/lib/src/plan.js +33 -0
  31. package/lib/src/plugin.d.ts +619 -0
  32. package/lib/src/plugin.js +205 -0
  33. package/lib/src/research.d.ts +78 -0
  34. package/lib/src/research.js +235 -0
  35. package/lib/src/schema.d.ts +52 -0
  36. package/lib/src/schema.js +82 -0
  37. package/lib/src/structured.d.ts +27 -0
  38. package/lib/src/structured.js +30 -0
  39. package/lib/src/workflow.d.ts +314 -0
  40. package/lib/src/workflow.js +171 -0
  41. package/lib/tsconfig.tsbuildinfo +1 -0
  42. package/package.json +218 -4
  43. package/src/assets.ts +248 -0
  44. package/src/config.ts +98 -0
  45. package/src/dashboard.ts +295 -0
  46. package/src/data.ts +167 -0
  47. package/src/defaults.ts +61 -0
  48. package/src/generate.ts +250 -0
  49. package/src/genie.ts +111 -0
  50. package/src/kpi.ts +106 -0
  51. package/src/manifest.json +72 -0
  52. package/src/namespace.ts +192 -0
  53. package/src/operations.ts +171 -0
  54. package/src/palette.ts +81 -0
  55. package/src/plan.ts +59 -0
  56. package/src/plugin.ts +244 -0
  57. package/src/research.ts +382 -0
  58. package/src/schema.ts +114 -0
  59. package/src/structured.ts +54 -0
  60. package/src/workflow.ts +221 -0
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Per-user Unity Catalog namespace derivation and provisioning.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import {
8
+ ConfigurationError,
9
+ createWorkspaceClient,
10
+ getExecutionContext,
11
+ type WorkspaceClient,
12
+ } from "@databricks/appkit";
13
+ import { databricks } from "@dbx-tools/appkit";
14
+ import { errorUtils, hash, stringUtils } from "@dbx-tools/shared-core";
15
+ import {
16
+ GeneratedSchemaSchema,
17
+ type GeneratedSchema,
18
+ type GeneratorRequest,
19
+ } from "@dbx-tools/shared-generator/workflow";
20
+
21
+ const OWNER_DIGEST_PROPERTY = "dbx_tools.generator.owner_digest";
22
+ const REQUEST_DIGEST_PROPERTY = "dbx_tools.generator.request_digest";
23
+
24
+ /** Inputs needed to provision one deterministic generated schema. */
25
+ export interface ProvisionSchemaInput {
26
+ /** Configured parent catalog. */
27
+ catalog: string;
28
+ /** Company and purpose used to derive the schema name. */
29
+ request: Pick<GeneratorRequest, "company" | "purpose">;
30
+ }
31
+
32
+ /** Trusted user identity and clients resolved from the active AppKit request. */
33
+ export interface GeneratorIdentity {
34
+ /** OBO workspace client. */
35
+ userClient: WorkspaceClient;
36
+ /** Service-principal workspace client. */
37
+ serviceClient: WorkspaceClient;
38
+ /** Trusted Databricks user name. */
39
+ principal: string;
40
+ /** Trusted stable user id. */
41
+ userId: string;
42
+ }
43
+
44
+ /** Derive a readable, bounded schema name with collision-resistant overflow. */
45
+ export function generatorSchemaName(principal: string, company: string, purpose: string): string {
46
+ const base = stringUtils.toIdentifierWithOptions(
47
+ {
48
+ delimiter: "_",
49
+ omitEmailDomain: true,
50
+ maxLength: 96,
51
+ truncateStrategy: "hash",
52
+ truncateHashLength: 8,
53
+ },
54
+ principal,
55
+ company,
56
+ purpose,
57
+ );
58
+ return /^[a-z]/.test(base) ? base : `u_${base}`;
59
+ }
60
+
61
+ /** Resolve the active OBO caller and a separate app service-principal client. */
62
+ export async function resolveGeneratorIdentity(): Promise<GeneratorIdentity> {
63
+ const context = getExecutionContext();
64
+ if (!("userId" in context)) {
65
+ throw new ConfigurationError("Generator operations require an on-behalf-of user context");
66
+ }
67
+ const current = await context.client.currentUser.me();
68
+ const principal = stringUtils.trimToNull(current.userName);
69
+ if (!principal) {
70
+ throw new ConfigurationError("The current Databricks user has no userName");
71
+ }
72
+ return {
73
+ userClient: context.client,
74
+ serviceClient: createWorkspaceClient(),
75
+ principal,
76
+ userId: context.userId,
77
+ };
78
+ }
79
+
80
+ /** Return an existing schema, treating only a genuine not-found response as absent. */
81
+ async function getSchema(
82
+ client: WorkspaceClient,
83
+ fullName: string,
84
+ ): Promise<
85
+ | Awaited<ReturnType<ReturnType<WorkspaceClient["toLegacyWorkspaceClient"]>["schemas"]["get"]>>
86
+ | undefined
87
+ > {
88
+ try {
89
+ return await client.toLegacyWorkspaceClient().schemas.get({ full_name: fullName });
90
+ } catch (cause) {
91
+ const context = errorUtils.errorContext(cause);
92
+ if (context.hasStatusCode(404) || context.hasMessage("does not exist")) return undefined;
93
+ throw cause;
94
+ }
95
+ }
96
+
97
+ /** Verify that an existing deterministic schema belongs to this user and request. */
98
+ function assertSchemaMarkers(
99
+ schema: { properties?: Record<string, string> },
100
+ ownerDigest: string,
101
+ requestDigest: string,
102
+ fullName: string,
103
+ ): void {
104
+ if (
105
+ schema.properties?.[OWNER_DIGEST_PROPERTY] !== ownerDigest ||
106
+ schema.properties?.[REQUEST_DIGEST_PROPERTY] !== requestDigest
107
+ ) {
108
+ throw new ConfigurationError(
109
+ `Generator schema ${fullName} already exists with different ownership markers`,
110
+ );
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Check-create-recheck one marked schema as the app service principal, then
116
+ * grant the OBO user full schema control while retaining SP ownership.
117
+ */
118
+ export async function provisionGeneratorSchema(
119
+ input: ProvisionSchemaInput,
120
+ signal?: AbortSignal,
121
+ ): Promise<GeneratedSchema> {
122
+ const identity = await resolveGeneratorIdentity();
123
+ return provisionGeneratorSchemaWithIdentity(input, identity, signal);
124
+ }
125
+
126
+ /** Provision with explicit trusted clients, for plugin reuse and isolated tests. */
127
+ export async function provisionGeneratorSchemaWithIdentity(
128
+ input: ProvisionSchemaInput,
129
+ identity: GeneratorIdentity,
130
+ signal?: AbortSignal,
131
+ ): Promise<GeneratedSchema> {
132
+ const catalog = stringUtils.trimToNull(input.catalog);
133
+ if (!catalog) throw new ConfigurationError("GENERATOR_CATALOG is required");
134
+
135
+ const schemaName = generatorSchemaName(
136
+ identity.principal,
137
+ input.request.company,
138
+ input.request.purpose,
139
+ );
140
+ const fullName = `${catalog}.${schemaName}`;
141
+ const ownerDigest = hash.fnvHash(identity.userId);
142
+ const requestDigest = hash.fnvHash(identity.userId, input.request.company, input.request.purpose);
143
+ const context = signal ? databricks.toContext(signal) : undefined;
144
+
145
+ await identity.userClient.toLegacyWorkspaceClient().catalogs.get({ name: catalog });
146
+
147
+ let schema = await getSchema(identity.serviceClient, fullName);
148
+ if (!schema) {
149
+ try {
150
+ await identity.serviceClient.toLegacyWorkspaceClient().schemas.create(
151
+ {
152
+ catalog_name: catalog,
153
+ name: schemaName,
154
+ comment: `Generated demo assets for ${identity.principal}`,
155
+ properties: {
156
+ [OWNER_DIGEST_PROPERTY]: ownerDigest,
157
+ [REQUEST_DIGEST_PROPERTY]: requestDigest,
158
+ },
159
+ },
160
+ context,
161
+ );
162
+ } catch (cause) {
163
+ if (!errorUtils.errorContext(cause).hasMessage("already exists")) throw cause;
164
+ }
165
+ schema = await getSchema(identity.serviceClient, fullName);
166
+ }
167
+ if (!schema) {
168
+ throw ConfigurationError.resourceNotFound("Generator schema", fullName);
169
+ }
170
+ assertSchemaMarkers(schema, ownerDigest, requestDigest, fullName);
171
+
172
+ await identity.serviceClient.toLegacyWorkspaceClient().grants.update(
173
+ {
174
+ securable_type: "SCHEMA",
175
+ full_name: fullName,
176
+ changes: [
177
+ {
178
+ principal: identity.principal,
179
+ add: ["ALL_PRIVILEGES", "MANAGE"],
180
+ },
181
+ ],
182
+ },
183
+ context,
184
+ );
185
+
186
+ return GeneratedSchemaSchema.parse({
187
+ catalog,
188
+ schema: schemaName,
189
+ fullName,
190
+ principal: identity.principal,
191
+ });
192
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Reusable generator operations shared by tools and workflow steps.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { ConfigurationError } from "@databricks/appkit";
8
+ import type { BrandGenerationRequest, BrandPackage } from "@dbx-tools/shared-generator/brand";
9
+ import type { KpiResearchRequest, KpiResearchResult } from "@dbx-tools/shared-generator/kpi";
10
+ import type {
11
+ CreateGeneratorPresentationRequest,
12
+ GeneratedDashboard,
13
+ GeneratedGenieSpace,
14
+ GeneratedSchema,
15
+ GeneratorPlan,
16
+ MaterializeGeneratorDataRequest,
17
+ MaterializedGeneratorData,
18
+ PlanGeneratorDataRequest,
19
+ ProvisionGeneratorSchemaRequest,
20
+ } from "@dbx-tools/shared-generator/workflow";
21
+ import type { ResolvedGeneratorConfig } from "./config.ts";
22
+ import { createGeneratorDashboard } from "./dashboard.ts";
23
+ import { materializeGeneratorData } from "./data.ts";
24
+ import { generateBrandPackage } from "./generate.ts";
25
+ import { createGeneratorGenieSpace } from "./genie.ts";
26
+ import { researchKpis } from "./kpi.ts";
27
+ import { provisionGeneratorSchema } from "./namespace.ts";
28
+ import { planGeneratorData } from "./plan.ts";
29
+ import type { BrandProgressReporter, BrandResearchClient } from "./research.ts";
30
+
31
+ /** Runtime dependencies captured by one generator plugin instance. */
32
+ export interface GeneratorOperationsOptions {
33
+ /** Resolved generator configuration. */
34
+ config: ResolvedGeneratorConfig;
35
+ /** Registered web-search plugin operations. */
36
+ researchClient: BrandResearchClient;
37
+ }
38
+
39
+ /** One behavior owner for every generator tool and workflow step. */
40
+ export class GeneratorOperations {
41
+ private readonly config: ResolvedGeneratorConfig;
42
+ private readonly researchClient: BrandResearchClient;
43
+
44
+ /** Create operations over one isolated plugin runtime. */
45
+ constructor(options: GeneratorOperationsOptions) {
46
+ this.config = options.config;
47
+ this.researchClient = options.researchClient;
48
+ }
49
+
50
+ /** Generate portable brand assets without KPI research or Databricks writes. */
51
+ generateBrand(
52
+ request: BrandGenerationRequest,
53
+ signal?: AbortSignal,
54
+ onProgress?: BrandProgressReporter,
55
+ ): Promise<BrandPackage> {
56
+ return generateBrandPackage(
57
+ request,
58
+ {
59
+ client: this.researchClient,
60
+ ...(this.config.model ? { model: this.config.model } : {}),
61
+ maxSources: this.config.maxSources,
62
+ sourceMaxLength: this.config.sourceMaxLength,
63
+ ...(onProgress ? { onProgress } : {}),
64
+ },
65
+ signal,
66
+ );
67
+ }
68
+
69
+ /** Research KPIs independently of any mutation workflow. */
70
+ researchKpis(request: KpiResearchRequest, signal?: AbortSignal): Promise<KpiResearchResult> {
71
+ return researchKpis(
72
+ request,
73
+ {
74
+ client: this.researchClient,
75
+ maxSources: this.config.maxSources,
76
+ sourceMaxLength: this.config.sourceMaxLength,
77
+ ...(this.config.model ? { model: this.config.model } : {}),
78
+ },
79
+ signal,
80
+ );
81
+ }
82
+
83
+ /** Build a typed deterministic data plan. */
84
+ planData(request: PlanGeneratorDataRequest, signal?: AbortSignal): Promise<GeneratorPlan> {
85
+ return planGeneratorData(
86
+ {
87
+ ...request,
88
+ ...(this.config.model ? { model: this.config.model } : {}),
89
+ },
90
+ signal,
91
+ );
92
+ }
93
+
94
+ /** Provision the active OBO user's deterministic schema. */
95
+ provisionSchema(
96
+ request: ProvisionGeneratorSchemaRequest,
97
+ signal?: AbortSignal,
98
+ ): Promise<GeneratedSchema> {
99
+ return provisionGeneratorSchema(
100
+ {
101
+ catalog: this.requiredCatalog(),
102
+ request: request.request,
103
+ },
104
+ signal,
105
+ );
106
+ }
107
+
108
+ /** Materialize generated data and validate its KPI aggregates. */
109
+ materializeData(
110
+ request: MaterializeGeneratorDataRequest,
111
+ signal?: AbortSignal,
112
+ ): Promise<MaterializedGeneratorData> {
113
+ return materializeGeneratorData(
114
+ request.location,
115
+ request.plan,
116
+ this.requiredWarehouseId(),
117
+ signal,
118
+ );
119
+ }
120
+
121
+ /** Create and publish a dashboard with automatic companion Genie. */
122
+ createDashboard(
123
+ request: CreateGeneratorPresentationRequest,
124
+ signal?: AbortSignal,
125
+ ): Promise<GeneratedDashboard> {
126
+ return createGeneratorDashboard(
127
+ {
128
+ location: request.location,
129
+ plan: request.plan,
130
+ brand: request.brand.brand,
131
+ company: request.request.company,
132
+ purpose: request.request.purpose,
133
+ warehouseId: this.requiredWarehouseId(),
134
+ },
135
+ signal,
136
+ );
137
+ }
138
+
139
+ /** Create a standalone Genie space for no-dashboard runs. */
140
+ createGenie(
141
+ request: CreateGeneratorPresentationRequest,
142
+ signal?: AbortSignal,
143
+ ): Promise<GeneratedGenieSpace> {
144
+ return createGeneratorGenieSpace(
145
+ {
146
+ location: request.location,
147
+ plan: request.plan,
148
+ company: request.request.company,
149
+ purpose: request.request.purpose,
150
+ warehouseId: this.requiredWarehouseId(),
151
+ },
152
+ signal,
153
+ );
154
+ }
155
+
156
+ /** Configured catalog or a deployment-time configuration error. */
157
+ private requiredCatalog(): string {
158
+ if (!this.config.catalog) {
159
+ throw new ConfigurationError("GENERATOR_CATALOG is required for generator mutations");
160
+ }
161
+ return this.config.catalog;
162
+ }
163
+
164
+ /** Configured SQL warehouse or a deployment-time configuration error. */
165
+ private requiredWarehouseId(): string {
166
+ if (!this.config.warehouseId) {
167
+ throw new ConfigurationError("DATABRICKS_WAREHOUSE_ID is required for generator mutations");
168
+ }
169
+ return this.config.warehouseId;
170
+ }
171
+ }
package/src/palette.ts ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Accessibility normalization for model-generated interface palettes.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import type { GeneratedBrandProfile } from "./schema.ts";
8
+
9
+ interface Rgb {
10
+ red: number;
11
+ green: number;
12
+ blue: number;
13
+ }
14
+
15
+ /** Expand a schema-validated hex color into RGB channels. */
16
+ function hexToRgb(color: string): Rgb {
17
+ const hex = color.slice(1);
18
+ const expanded =
19
+ hex.length === 3 ? [...hex].map((value) => `${value}${value}`).join("") : hex.slice(0, 6);
20
+ return {
21
+ red: Number.parseInt(expanded.slice(0, 2), 16),
22
+ green: Number.parseInt(expanded.slice(2, 4), 16),
23
+ blue: Number.parseInt(expanded.slice(4, 6), 16),
24
+ };
25
+ }
26
+
27
+ /** Serialize RGB channels as a six-digit uppercase hex color. */
28
+ function rgbToHex(color: Rgb): string {
29
+ const channel = (value: number) => Math.round(value).toString(16).padStart(2, "0").toUpperCase();
30
+ return `#${channel(color.red)}${channel(color.green)}${channel(color.blue)}`;
31
+ }
32
+
33
+ /** WCAG relative luminance for one sRGB color. */
34
+ function relativeLuminance(color: Rgb): number {
35
+ const channel = (value: number) => {
36
+ const normalized = value / 255;
37
+ return normalized <= 0.04045 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4;
38
+ };
39
+ return 0.2126 * channel(color.red) + 0.7152 * channel(color.green) + 0.0722 * channel(color.blue);
40
+ }
41
+
42
+ /** WCAG contrast ratio between two schema-valid hex colors. */
43
+ export function colorContrastRatio(foreground: string, background: string): number {
44
+ const first = relativeLuminance(hexToRgb(foreground));
45
+ const second = relativeLuminance(hexToRgb(background));
46
+ const lighter = Math.max(first, second);
47
+ const darker = Math.min(first, second);
48
+ return (lighter + 0.05) / (darker + 0.05);
49
+ }
50
+
51
+ /** Blend a color toward black or white past the AA threshold with rendering margin. */
52
+ function ensureContrast(color: string, background: string, minimum = 5): string {
53
+ if (colorContrastRatio(color, background) >= minimum) return color;
54
+ const source = hexToRgb(color);
55
+ const backgroundIsLight = relativeLuminance(hexToRgb(background)) > 0.5;
56
+ const target = backgroundIsLight ? 0 : 255;
57
+ for (let amount = 0.05; amount <= 1; amount += 0.05) {
58
+ const candidate = rgbToHex({
59
+ red: source.red + (target - source.red) * amount,
60
+ green: source.green + (target - source.green) * amount,
61
+ blue: source.blue + (target - source.blue) * amount,
62
+ });
63
+ if (colorContrastRatio(candidate, background) >= minimum) return candidate;
64
+ }
65
+ return backgroundIsLight ? "#000000" : "#FFFFFF";
66
+ }
67
+
68
+ /** Make generated text and action colors readable against the page background. */
69
+ export function normalizeBrandPalette(profile: GeneratedBrandProfile): GeneratedBrandProfile {
70
+ const background = profile.colors.background;
71
+ return {
72
+ ...profile,
73
+ colors: {
74
+ ...profile.colors,
75
+ primary: ensureContrast(profile.colors.primary, background),
76
+ primaryHover: ensureContrast(profile.colors.primaryHover, background),
77
+ foreground: ensureContrast(profile.colors.foreground, background),
78
+ muted: ensureContrast(profile.colors.muted, background),
79
+ },
80
+ };
81
+ }
package/src/plan.ts ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Structured demo-data planning from brand and KPI research.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import type { BrandPackage } from "@dbx-tools/shared-generator/brand";
8
+ import type { KpiResearchResult } from "@dbx-tools/shared-generator/kpi";
9
+ import {
10
+ GeneratorPlanSchema,
11
+ type GeneratorPlan,
12
+ type GeneratorRequest,
13
+ } from "@dbx-tools/shared-generator/workflow";
14
+ import { generateStructured } from "./structured.ts";
15
+
16
+ /** Inputs for deterministic demo-data planning. */
17
+ export interface PlanGeneratorDataInput {
18
+ /** Validated workflow request. */
19
+ request: GeneratorRequest;
20
+ /** Researched brand package. */
21
+ brand: BrandPackage;
22
+ /** Evidence-backed KPI research. */
23
+ kpiResearch: KpiResearchResult;
24
+ /** Optional model preference. */
25
+ model?: string;
26
+ }
27
+
28
+ /** Build a constrained plan that can be compiled without model-authored SQL. */
29
+ export async function planGeneratorData(
30
+ input: PlanGeneratorDataInput,
31
+ signal?: AbortSignal,
32
+ ): Promise<GeneratorPlan> {
33
+ const generated = await generateStructured({
34
+ id: "demo-data-planner",
35
+ instructions: [
36
+ "Plan one denormalized Delta fact table for a realistic business demo.",
37
+ "Use only the supplied generator kinds and KPI aggregations.",
38
+ "Include an id column, a date column, two to six useful categories,",
39
+ "numeric columns needed by every KPI, and a measurable anomaly.",
40
+ "Do not write SQL. Do not invent unsupported generator fields.",
41
+ ].join(" "),
42
+ prompt: JSON.stringify(
43
+ {
44
+ request: input.request,
45
+ brand: input.brand.brand,
46
+ kpis: input.kpiResearch.kpis,
47
+ },
48
+ null,
49
+ 2,
50
+ ),
51
+ schema: GeneratorPlanSchema,
52
+ ...(input.model ? { model: input.model } : {}),
53
+ ...(signal ? { signal } : {}),
54
+ });
55
+ return GeneratorPlanSchema.parse({
56
+ ...generated,
57
+ rowCount: input.request.rowCount,
58
+ });
59
+ }