@foundrynorth/compass-schema 1.0.13 → 1.0.15

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foundrynorth/compass-schema",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "Canonical Drizzle ORM schema for Foundry Compass (rough-waterfall database)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/relations.ts CHANGED
@@ -15,6 +15,11 @@ import {
15
15
  semCampaigns,
16
16
  mediaOrders,
17
17
  mediaOrderLineItems,
18
+ planOptions,
19
+ researchSnapshots,
20
+ researchRuns,
21
+ engagements,
22
+ planConfigurations,
18
23
  } from "./schema.js";
19
24
 
20
25
  export const deepEnrichmentUsageRelations = relations(deepEnrichmentUsage, ({one}) => ({
@@ -24,9 +29,16 @@ export const deepEnrichmentUsageRelations = relations(deepEnrichmentUsage, ({one
24
29
  }),
25
30
  }));
26
31
 
27
- export const plansRelations = relations(plans, ({many}) => ({
32
+ export const plansRelations = relations(plans, ({one, many}) => ({
28
33
  deepEnrichmentUsages: many(deepEnrichmentUsage),
29
34
  planEnrichments: many(planEnrichments),
35
+ planOptions: many(planOptions),
36
+ researchSnapshots: many(researchSnapshots),
37
+ researchRuns: many(researchRuns),
38
+ selectedOption: one(planOptions, {
39
+ fields: [plans.selectedOptionId],
40
+ references: [planOptions.id],
41
+ }),
30
42
  }));
31
43
 
32
44
  export const pdfTemplateFieldMappingsRelations = relations(pdfTemplateFieldMappings, ({one}) => ({
@@ -98,6 +110,54 @@ export const googleAdSearchesRelations = relations(googleAdSearches, ({many}) =>
98
110
  googleAdCreatives: many(googleAdCreatives),
99
111
  }));
100
112
 
113
+ /** Canonical planning model relations */
114
+ export const researchSnapshotsRelations = relations(researchSnapshots, ({one, many}) => ({
115
+ plan: one(plans, {
116
+ fields: [researchSnapshots.planId],
117
+ references: [plans.id],
118
+ }),
119
+ engagement: one(engagements, {
120
+ fields: [researchSnapshots.engagementId],
121
+ references: [engagements.id],
122
+ }),
123
+ planOptions: many(planOptions),
124
+ researchRuns: many(researchRuns),
125
+ }));
126
+
127
+ export const planOptionsRelations = relations(planOptions, ({one}) => ({
128
+ plan: one(plans, {
129
+ fields: [planOptions.planId],
130
+ references: [plans.id],
131
+ }),
132
+ engagement: one(engagements, {
133
+ fields: [planOptions.engagementId],
134
+ references: [engagements.id],
135
+ }),
136
+ legacyConfiguration: one(planConfigurations, {
137
+ fields: [planOptions.legacyConfigurationId],
138
+ references: [planConfigurations.id],
139
+ }),
140
+ researchSnapshot: one(researchSnapshots, {
141
+ fields: [planOptions.researchSnapshotId],
142
+ references: [researchSnapshots.id],
143
+ }),
144
+ }));
145
+
146
+ export const researchRunsRelations = relations(researchRuns, ({one}) => ({
147
+ plan: one(plans, {
148
+ fields: [researchRuns.planId],
149
+ references: [plans.id],
150
+ }),
151
+ engagement: one(engagements, {
152
+ fields: [researchRuns.engagementId],
153
+ references: [engagements.id],
154
+ }),
155
+ researchSnapshot: one(researchSnapshots, {
156
+ fields: [researchRuns.researchSnapshotId],
157
+ references: [researchSnapshots.id],
158
+ }),
159
+ }));
160
+
101
161
  /** SEM campaign relations — links to media order and line item. */
102
162
  export const semCampaignsRelations = relations(semCampaigns, ({ one }) => ({
103
163
  mediaOrder: one(mediaOrders, {
package/src/schema.ts CHANGED
@@ -1025,6 +1025,7 @@ export const plans = pgTable("plans", {
1025
1025
  // Partner association (null = legacy plans before partner system)
1026
1026
  partnerId: varchar("partner_id"),
1027
1027
  engagementId: varchar("engagement_id"), // FK → engagements.id (nullable for backward compatibility)
1028
+ selectedOptionId: varchar("selected_option_id"), // FK → plan_options.id (nullable during migration)
1028
1029
  contractId: uuid("contract_id"), // FK → contracts.id (nullable — links to annual contract)
1029
1030
 
1030
1031
  // Workflow mode: determines which UI sections and features are available
@@ -1255,6 +1256,7 @@ export const proposals = pgTable(
1255
1256
 
1256
1257
  // Configuration references (which plan_configurations to include)
1257
1258
  configurationIds: jsonb("configuration_ids").$type<string[]>(),
1259
+ optionIds: jsonb("option_ids").$type<string[]>(),
1258
1260
 
1259
1261
  // Organization
1260
1262
  partnerId: varchar("partner_id"),
@@ -1363,6 +1365,195 @@ export const insertPlanConfigurationSchema = createInsertSchema(planConfiguratio
1363
1365
  export type InsertPlanConfiguration = z.infer<typeof insertPlanConfigurationSchema>;
1364
1366
  export type PlanConfiguration = typeof planConfigurations.$inferSelect;
1365
1367
 
1368
+ // Canonical planning model enums
1369
+ export const planOptionSourceEnum = pgEnum("plan_option_source", [
1370
+ "strategist",
1371
+ "ai",
1372
+ "derived_snapshot",
1373
+ ]);
1374
+
1375
+ export const planOptionKindEnum = pgEnum("plan_option_kind", [
1376
+ "base",
1377
+ "alternative",
1378
+ ]);
1379
+
1380
+ export const planOptionStatusEnum = pgEnum("plan_option_status", [
1381
+ "draft",
1382
+ "recommended",
1383
+ "selected",
1384
+ "ordered",
1385
+ "archived",
1386
+ ]);
1387
+
1388
+ export const researchRunStatusEnum = pgEnum("research_run_status", [
1389
+ "queued",
1390
+ "running",
1391
+ "completed",
1392
+ "failed",
1393
+ "cancelled",
1394
+ "reused",
1395
+ ]);
1396
+
1397
+ // Canonical research snapshot store
1398
+ export const researchSnapshots = pgTable(
1399
+ "research_snapshots",
1400
+ {
1401
+ id: varchar("id")
1402
+ .primaryKey()
1403
+ .default(sql`gen_random_uuid()`),
1404
+ engagementId: varchar("engagement_id").references(() => engagements.id, {
1405
+ onDelete: "set null",
1406
+ }),
1407
+ planId: varchar("plan_id")
1408
+ .notNull()
1409
+ .references(() => plans.id, { onDelete: "cascade" }),
1410
+ scopeType: text("scope_type").notNull(),
1411
+ scopeKey: text("scope_key").notNull(),
1412
+ sourceMode: text("source_mode").notNull(),
1413
+ snapshotVersion: integer("snapshot_version").notNull().default(1),
1414
+ prospectProfile: jsonb("prospect_profile").$type<Record<string, any>>(),
1415
+ competitiveContext: jsonb("competitive_context").$type<Record<string, any>>(),
1416
+ marketContext: jsonb("market_context").$type<Record<string, any>>(),
1417
+ constraints: jsonb("constraints").$type<Record<string, any>>(),
1418
+ fulfillmentContext: jsonb("fulfillment_context").$type<Record<string, any>>(),
1419
+ brandContext: jsonb("brand_context").$type<Record<string, any>>(),
1420
+ provenance: jsonb("provenance").$type<Record<string, any>>().notNull(),
1421
+ freshUntil: timestamp("fresh_until"),
1422
+ reuseHash: text("reuse_hash"),
1423
+ triggerRunId: varchar("trigger_run_id"),
1424
+ costMeta: jsonb("cost_meta").$type<Record<string, any>>(),
1425
+ createdAt: timestamp("created_at").notNull().defaultNow(),
1426
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
1427
+ },
1428
+ (table) => [
1429
+ index("research_snapshots_plan_id_idx").on(table.planId),
1430
+ index("research_snapshots_plan_version_idx").on(table.planId, table.snapshotVersion),
1431
+ index("research_snapshots_scope_key_idx").on(table.scopeKey),
1432
+ index("research_snapshots_reuse_hash_idx").on(table.reuseHash),
1433
+ ]
1434
+ );
1435
+
1436
+ export const insertResearchSnapshotSchema = createInsertSchema(researchSnapshots).omit({
1437
+ id: true,
1438
+ createdAt: true,
1439
+ updatedAt: true,
1440
+ });
1441
+
1442
+ export type InsertResearchSnapshot = z.infer<typeof insertResearchSnapshotSchema>;
1443
+ export type ResearchSnapshot = typeof researchSnapshots.$inferSelect;
1444
+
1445
+ // Canonical actionable option store
1446
+ export const planOptions = pgTable(
1447
+ "plan_options",
1448
+ {
1449
+ id: varchar("id")
1450
+ .primaryKey()
1451
+ .default(sql`gen_random_uuid()`),
1452
+ planId: varchar("plan_id")
1453
+ .notNull()
1454
+ .references(() => plans.id, { onDelete: "cascade" }),
1455
+ engagementId: varchar("engagement_id").references(() => engagements.id, {
1456
+ onDelete: "set null",
1457
+ }),
1458
+ legacyConfigurationId: varchar("legacy_configuration_id").references(
1459
+ () => planConfigurations.id,
1460
+ { onDelete: "set null" }
1461
+ ),
1462
+ legacyTierKey: text("legacy_tier_key"),
1463
+ name: text("name").notNull(),
1464
+ label: text("label"),
1465
+ source: planOptionSourceEnum("source").notNull(),
1466
+ kind: planOptionKindEnum("kind").notNull(),
1467
+ status: planOptionStatusEnum("status").notNull().default("draft"),
1468
+ derivedFromOptionId: varchar("derived_from_option_id"),
1469
+ researchSnapshotId: varchar("research_snapshot_id").references(
1470
+ () => researchSnapshots.id,
1471
+ { onDelete: "set null" }
1472
+ ),
1473
+ generationRunId: varchar("generation_run_id"),
1474
+ budgetTotal: integer("budget_total"),
1475
+ durationMonths: integer("duration_months"),
1476
+ allocations: jsonb("allocations")
1477
+ .$type<Record<string, any>[]>()
1478
+ .notNull()
1479
+ .default(sql`'[]'::jsonb`),
1480
+ rationale: text("rationale"),
1481
+ assumptions: jsonb("assumptions").$type<Record<string, any>>(),
1482
+ constraints: jsonb("constraints").$type<Record<string, any>>(),
1483
+ fulfillmentNotes: text("fulfillment_notes"),
1484
+ generationContext: jsonb("generation_context").$type<Record<string, any>>(),
1485
+ displayOrder: integer("display_order").notNull().default(0),
1486
+ createdBy: text("created_by"),
1487
+ createdAt: timestamp("created_at").notNull().defaultNow(),
1488
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
1489
+ },
1490
+ (table) => [
1491
+ index("plan_options_plan_id_idx").on(table.planId),
1492
+ index("plan_options_plan_display_order_idx").on(table.planId, table.displayOrder),
1493
+ index("plan_options_engagement_id_idx").on(table.engagementId),
1494
+ index("plan_options_research_snapshot_id_idx").on(table.researchSnapshotId),
1495
+ uniqueIndex("plan_options_one_recommended_idx")
1496
+ .on(table.planId)
1497
+ .where(sql`${table.status} = 'recommended'::plan_option_status`),
1498
+ uniqueIndex("plan_options_one_selected_idx")
1499
+ .on(table.planId)
1500
+ .where(sql`${table.status} = 'selected'::plan_option_status`),
1501
+ ]
1502
+ );
1503
+
1504
+ export const insertPlanOptionSchema = createInsertSchema(planOptions).omit({
1505
+ id: true,
1506
+ createdAt: true,
1507
+ updatedAt: true,
1508
+ });
1509
+
1510
+ export type InsertPlanOption = z.infer<typeof insertPlanOptionSchema>;
1511
+ export type PlanOption = typeof planOptions.$inferSelect;
1512
+
1513
+ // Operational ledger for research runs and snapshot materialization
1514
+ export const researchRuns = pgTable(
1515
+ "research_runs",
1516
+ {
1517
+ id: varchar("id")
1518
+ .primaryKey()
1519
+ .default(sql`gen_random_uuid()`),
1520
+ triggerRunId: varchar("trigger_run_id"),
1521
+ taskId: text("task_id").notNull(),
1522
+ engagementId: varchar("engagement_id").references(() => engagements.id, {
1523
+ onDelete: "set null",
1524
+ }),
1525
+ planId: varchar("plan_id").references(() => plans.id, {
1526
+ onDelete: "set null",
1527
+ }),
1528
+ researchSnapshotId: varchar("research_snapshot_id").references(
1529
+ () => researchSnapshots.id,
1530
+ { onDelete: "set null" }
1531
+ ),
1532
+ status: researchRunStatusEnum("status").notNull().default("queued"),
1533
+ requestedBy: text("requested_by"),
1534
+ runReason: text("run_reason"),
1535
+ inputHash: text("input_hash"),
1536
+ costMeta: jsonb("cost_meta").$type<Record<string, any>>(),
1537
+ startedAt: timestamp("started_at"),
1538
+ completedAt: timestamp("completed_at"),
1539
+ error: text("error"),
1540
+ createdAt: timestamp("created_at").notNull().defaultNow(),
1541
+ },
1542
+ (table) => [
1543
+ index("research_runs_plan_id_idx").on(table.planId),
1544
+ index("research_runs_trigger_run_id_idx").on(table.triggerRunId),
1545
+ index("research_runs_task_status_idx").on(table.taskId, table.status),
1546
+ ]
1547
+ );
1548
+
1549
+ export const insertResearchRunSchema = createInsertSchema(researchRuns).omit({
1550
+ id: true,
1551
+ createdAt: true,
1552
+ });
1553
+
1554
+ export type InsertResearchRun = z.infer<typeof insertResearchRunSchema>;
1555
+ export type ResearchRun = typeof researchRuns.$inferSelect;
1556
+
1366
1557
  // Jobs Table for async operations
1367
1558
  export const jobs = pgTable("jobs", {
1368
1559
  id: varchar("id")
@@ -4525,6 +4716,7 @@ export const mediaOrders = pgTable(
4525
4716
  planId: varchar("plan_id"), // FK → plans.id (nullable — can exist without a plan)
4526
4717
  engagementId: varchar("engagement_id"), // FK → engagements.id (nullable during transition)
4527
4718
  configurationId: varchar("configuration_id"), // FK → plan_configurations.id (named product mix)
4719
+ optionId: varchar("option_id"), // FK → plan_options.id (canonical selected option)
4528
4720
  contractId: uuid("contract_id"), // FK → contracts.id (nullable — links to annual contract)
4529
4721
  partnerId: varchar("partner_id"),
4530
4722
  hubspotDealId: text("hubspot_deal_id"),
@@ -8003,3 +8195,14 @@ export const compassEventOutbox = pgTable(
8003
8195
 
8004
8196
  export type CompassEventOutboxRecord = typeof compassEventOutbox.$inferSelect;
8005
8197
  export type InsertCompassEventOutbox = typeof compassEventOutbox.$inferInsert;
8198
+
8199
+ // ─── Creative Deliveries (Prism webhook idempotency) ────────────────────────
8200
+
8201
+ export const creativeDeliveries = pgTable("creative_deliveries", {
8202
+ id: serial("id").primaryKey(),
8203
+ packageId: varchar("package_id", { length: 255 }).notNull().unique(),
8204
+ compassCampaignId: varchar("compass_campaign_id", { length: 255 }).notNull(),
8205
+ status: varchar("status", { length: 50 }).notNull(),
8206
+ assetCount: integer("asset_count").notNull().default(0),
8207
+ receivedAt: timestamp("received_at").defaultNow().notNull(),
8208
+ });