mike boone commited on
Commit
faddd50
·
1 Parent(s): 667cd22

fix: improve specialized dataset dimensions

Browse files
DEMOPREP_SELLING_STATUS.md ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # DemoPrep Status Brief
2
+
3
+ ## Purpose
4
+
5
+ DemoPrep is a demo-generation application built to help sales and solution teams create relevant, live analytics demos quickly. Given a company and a use case, the application researches the business context, builds a realistic analytics dataset, deploys it to Snowflake, creates a ThoughtSpot model, and generates a liveboard that can be used in customer conversations.
6
+
7
+ The goal is simple: reduce the time and friction required to create credible, company-specific demos, while improving demo quality and consistency across the field.
8
+
9
+ ## What We Built
10
+
11
+ DemoPrep now supports an end-to-end workflow from company input to live ThoughtSpot assets:
12
+
13
+ 1. **Company and use-case intake**
14
+ Users provide a target company, select a vertical / line / function, or enter a custom business context.
15
+
16
+ 2. **Research and context generation**
17
+ The system gathers business context and uses it to shape the demo narrative, metrics, dimensions, and liveboard story.
18
+
19
+ 3. **Dataset-first generation**
20
+ We moved away from purely schema-first synthetic data generation. The current approach builds coherent business datasets first, then derives the schema and DDL from that dataset. This has materially improved realism and reduced impossible metric combinations.
21
+
22
+ 4. **Snowflake deployment**
23
+ DemoPrep creates the schema and loads populated tables into Snowflake.
24
+
25
+ 5. **ThoughtSpot deployment**
26
+ The application creates the connection, tables, joins, model, and liveboard. The liveboard path uses MCP creation followed by TML post-processing for layout and polish.
27
+
28
+ 6. **Quality test harness**
29
+ We built an end-to-end Playwright quality suite that submits demo requests, waits for the pipeline, resolves the exact ThoughtSpot model/liveboard, samples Snowflake data, and grades both data quality and liveboard quality with an AI rubric.
30
+
31
+ ## Current Status
32
+
33
+ The application is now consistently completing end-to-end runs in local testing, with meaningful improvements in data quality.
34
+
35
+ Recent local six-pack result:
36
+
37
+ | Test | Data | Liveboard | Total |
38
+ |------|------|-----------|-------|
39
+ | EY Professional Services | 76 | 64 | 79.0 / B |
40
+ | Datadog SaaS Sales | 70 | 73 | 78.2 / B |
41
+ | Nike Retail Sales | 80 | 73 | 83.2 / B |
42
+ | Delta Airline Sales | 65 | 74 | 76.0 / B |
43
+ | Wells Fargo Banking Marketing | 78 | 69 | 81.2 / B |
44
+ | Starbucks Store Operations | 72 | 58 | 75.5 / B |
45
+
46
+ Average: **78.9 / B**
47
+
48
+ This is a major improvement from earlier runs where data quality was frequently in the 40s and 50s, with common issues such as returns exceeding sales, load factors over 100%, marketing funnel metrics out of order, and generic data that did not match the target company or use case.
49
+
50
+ ## What Improved
51
+
52
+ The biggest improvement is the move toward a more coherent data-generation approach:
53
+
54
+ - **Dataset-first architecture**: We now generate business-realistic datasets before deriving DDL.
55
+ - **Scenario families**: The system maps requests into known scenario families such as SaaS sales, retail sales, banking marketing, airline operations, store operations, sports venue engagement, and professional services.
56
+ - **Custom use case handling**: Custom prompts now influence the dataset structure more strongly. For example, EY now generates professional services data instead of incorrectly producing sports and venue data.
57
+ - **Better test identity**: The test runner now uses page URLs and exact run context instead of schema-prefix guessing.
58
+ - **More reliable diagnostics**: Logging has been improved so run identity, model selection, stage completion, and errors are easier to trace.
59
+ - **MCP contingency path**: If the ThoughtSpot MCP service is unavailable, DemoPrep can create a clearly marked backup liveboard path rather than silently hiding the failure.
60
+
61
+ ## Why This Matters Commercially
62
+
63
+ Demo quality directly affects seller confidence and customer credibility.
64
+
65
+ Today, high-quality custom demos require manual work from people who understand the account, the industry, the data model, and ThoughtSpot. That creates a bottleneck. DemoPrep reduces that bottleneck by turning a company and business problem into a working analytics experience.
66
+
67
+ The commercial value is:
68
+
69
+ - **Faster demo creation**: Move from days or weeks of prep toward same-day demo generation.
70
+ - **Better account relevance**: Generate demos that reflect the prospect’s company, industry, metrics, and business questions.
71
+ - **Repeatable quality**: Create a structured path for demos instead of relying on ad hoc manual builds.
72
+ - **Field leverage**: Give more sellers and solution engineers access to credible demos without requiring every demo to be hand-built.
73
+ - **Higher confidence in AI demos**: The quality harness creates a feedback loop so we can measure and improve the demos instead of relying on subjective inspection.
74
+
75
+ ## Remaining Gaps
76
+
77
+ The application is not finished, but the core value is now visible.
78
+
79
+ Current gaps:
80
+
81
+ - **Liveboard quality trails data quality**: Data scores are now often B-level, while liveboards still need better narrative layout, stronger KPI selection, and more consistent visual variety.
82
+ - **Some scenarios are still generic**: Certain industry/function combinations need deeper specialization to feel truly customer-specific.
83
+ - **Custom extraction needs hardening**: We improved this significantly, but custom prompts still need more testing across unusual industries and ambiguous company contexts.
84
+ - **Operational resilience**: ThoughtSpot deployment and MCP availability remain external dependencies. We now handle some failures better, but this should continue to be hardened.
85
+ - **Packaging and ownership**: To sell or scale this internally, we need clearer ownership, deployment expectations, support model, and roadmap.
86
+
87
+ ## Call To Action
88
+
89
+ We are asking for alignment on whether DemoPrep should move from a sprint prototype into a supported internal product motion.
90
+
91
+ Specific asks:
92
+
93
+ 1. **Executive sponsorship**
94
+ Confirm that rapid AI-generated demo creation is a priority worth operationalizing.
95
+
96
+ 2. **Pilot group**
97
+ Identify a small group of sellers and solution engineers to use DemoPrep on real opportunities and provide structured feedback.
98
+
99
+ 3. **Quality bar**
100
+ Agree on what “demo-ready” means. Proposed starting bar: pipeline completion, data score 75+, liveboard score 70+, and no critical business-logic violations.
101
+
102
+ 4. **Scenario prioritization**
103
+ Pick the highest-value vertical/function combinations to harden first based on pipeline impact.
104
+
105
+ 5. **Production path**
106
+ Decide whether this should remain a specialist tool, become an internal field application, or eventually become a customer-facing/productized capability.
107
+
108
+ ## Recommended Follow-Up
109
+
110
+ After the meeting, the recommended next steps are:
111
+
112
+ - Run a structured pilot with 5-10 field users.
113
+ - Select 10 priority demo scenarios and harden them to a consistent quality bar.
114
+ - Improve liveboard generation and post-processing so visual storytelling catches up to dataset quality.
115
+ - Add a dashboard for quality trends, failure reasons, scenario coverage, and user feedback.
116
+ - Define ownership for deployment, monitoring, support, and roadmap.
117
+ - Prepare a short internal demo showing before/after quality improvements and a live end-to-end generation.
118
+
119
+ ## Bottom Line
120
+
121
+ DemoPrep is now showing the shape of a valuable internal sales enablement platform. It can generate complete ThoughtSpot demos from company and use-case input, and recent work has materially improved data quality and pipeline reliability.
122
+
123
+ The next decision is whether we want to treat this as an experiment, or invest enough to make it a reliable tool for the field.
demoprep_app/dataset/generators/template_generator.py CHANGED
@@ -183,6 +183,41 @@ class TemplateDatasetGenerator(ScenarioDatasetGenerator):
183
  "TEST_DRIVES": self._whole(rng, vehicles, max(vehicles + 1, vehicles * 5), 1.0),
184
  }
185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  if profile == "apparel_marketing":
187
  impressions = self._whole(rng, 25_000, 1_200_000, mult)
188
  clicks = self._bounded_child(rng, impressions, 0.012, 0.075)
@@ -690,18 +725,230 @@ def _dimension_values(name: str, company_name: str, context: str = "") -> list[s
690
  context_low = (context or "").lower()
691
  shipping_context = any(term in context_low for term in ("shipping", "shipment", "parcel", "freight", "delivery"))
692
  life_sciences_context = any(term in context_low for term in ("life sciences", "pharma", "medical device", "therapy", "biotech", "healthcare"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
693
  service_values = (
694
  ["Ground", "Express", "Freight", "International Priority", "Last Mile", "Returns", "E-Commerce Fulfillment", "Healthcare Logistics"]
695
  if shipping_context
696
  else ["API Gateway", "Web App", "Data Platform", "Identity", "Payments", "Reporting", "Observability", "Collaboration"]
697
  )
698
- product_values = (
699
- ["Oncology Therapy", "Immunology Therapy", "Surgical Device", "Orthopedic Implant", "Diagnostic Kit", "Cardiovascular Device", "Vision Care", "Consumer Health"]
700
- if life_sciences_context
701
- else [f"{company} Core", f"{company} Plus", f"{company} Enterprise", f"{company} Analytics", f"{company} Services", f"{company} Mobile"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702
  )
703
  values = {
704
- "REGIONS": ["North America", "EMEA", "APAC", "Latin America", "Public Sector", "Enterprise East", "Enterprise West", "Digital"],
705
  "PRODUCTS": product_values,
706
  "CUSTOMERS": ["Enterprise", "Mid-Market", "SMB", "Strategic", "Commercial", "Public Sector"],
707
  "ACCOUNTS": ["Apex Systems", "Northstar Health", "Summit Retail", "Vertex Bank", "Beacon Logistics", "Cloudline Media"],
@@ -722,8 +969,8 @@ def _dimension_values(name: str, company_name: str, context: str = "") -> list[s
722
  "BRANDS": ["Luxury", "Premium", "Select Service", "Extended Stay", "Resort", "Convention", "Lifestyle", "Boutique"],
723
  "PRODUCT_CATEGORIES": ["Denim", "Outerwear", "Sportswear", "Dresses", "Footwear", "Accessories", "Kids", "Underwear"],
724
  "STORES": [f"{company} Flagship", f"{company} Core", f"{company} Outlet", f"{company} Digital", f"{company} Urban", f"{company} Suburban"],
725
- "WAREHOUSES": ["Northeast DC", "Southeast DC", "Midwest DC", "Southwest DC", "West DC", "Import Hub"],
726
- "SUPPLIERS": ["Preferred Supply", "Global Components", "Prime Source", "North Coast", "Vertex Manufacturing", "Reliable Partners"],
727
  "ROUTES": ["Northeast Corridor", "West Coast Loop", "Central Express", "Transatlantic", "Pacific Shuttle", "Southern Line"],
728
  "CARRIERS": ["Rapid Freight", "Prime Carrier", "ExpressLine", "Metro Delivery", "AirBridge", "Last Mile Co"],
729
  "AIRPORTS": ["JFK", "LAX", "ORD", "DFW", "ATL", "SFO"],
@@ -734,7 +981,10 @@ def _dimension_values(name: str, company_name: str, context: str = "") -> list[s
734
  "SERVICE_LINES": service_lines,
735
  "DEPARTMENTS": ["Engineering", "Sales", "Operations", "Finance", "Support", "Marketing"],
736
  "MATTERS": ["Contract Review", "Employment Claim", "Litigation", "Compliance Audit", "IP Filing", "Privacy Review"],
737
- "PROGRAMS": ["Business", "Computer Science", "Nursing", "Data Analytics", "Education", "Engineering"],
 
 
 
738
  "TEAMS": context_values or [f"{company} Home Team", f"{company} Visiting Team", f"{company} Women's Team", f"{company} Development Team", f"{company} League Event", f"{company} Exhibition"],
739
  "VENUES": context_values or [company, f"{company} Arena", f"{company} Theater", f"{company} Premium Club", f"{company} Event Hall", f"{company} Fan Zone"],
740
  "EVENTS": context_values or [f"{company} Regular Season", f"{company} Premium Hospitality", f"{company} Group Sales Night", f"{company} Concert", f"{company} Theater Event", f"{company} Family Show"],
 
183
  "TEST_DRIVES": self._whole(rng, vehicles, max(vehicles + 1, vehicles * 5), 1.0),
184
  }
185
 
186
+ if profile == "automotive_supplier_sales":
187
+ quoted_units = self._whole(rng, 2_500, 420_000, mult)
188
+ award_rate = rng.uniform(0.18, 0.72)
189
+ awarded_units = max(0, int(quoted_units * award_rate))
190
+ asp = round(rng.uniform(42, 1_850), 2)
191
+ quoted_revenue = round(quoted_units * asp, 2)
192
+ awarded_revenue = round(awarded_units * asp, 2)
193
+ material_cost = round(awarded_revenue * rng.uniform(0.46, 0.68), 2)
194
+ manufacturing_cost = round(awarded_revenue * rng.uniform(0.12, 0.26), 2)
195
+ tooling_recovery = round(awarded_revenue * rng.uniform(0.01, 0.08), 2)
196
+ warranty_reserve = round(awarded_revenue * rng.uniform(0.004, 0.035), 2)
197
+ margin = round(awarded_revenue + tooling_recovery - material_cost - manufacturing_cost - warranty_reserve, 2)
198
+ backlog = round(awarded_revenue * rng.uniform(0.45, 2.8), 2)
199
+ on_time_launch = round(rng.uniform(72, 98), 2)
200
+ ppm = round(rng.uniform(8, 240), 2)
201
+ return {
202
+ "RFQS_RECEIVED": rng.randint(1, 18),
203
+ "QUOTED_UNITS": quoted_units,
204
+ "AWARDED_UNITS": awarded_units,
205
+ "QUOTE_WIN_RATE_PCT": round(award_rate * 100, 2),
206
+ "AVG_SELLING_PRICE_USD": asp,
207
+ "QUOTED_REVENUE_USD": quoted_revenue,
208
+ "AWARDED_REVENUE_USD": awarded_revenue,
209
+ "BACKLOG_USD": backlog,
210
+ "MATERIAL_COST_USD": material_cost,
211
+ "MANUFACTURING_COST_USD": manufacturing_cost,
212
+ "TOOLING_RECOVERY_USD": tooling_recovery,
213
+ "WARRANTY_RESERVE_USD": warranty_reserve,
214
+ "PROGRAM_MARGIN_USD": margin,
215
+ "PROGRAM_MARGIN_PCT": self._pct(margin, awarded_revenue),
216
+ "ON_TIME_LAUNCH_PCT": on_time_launch,
217
+ "DEFECT_PPM": ppm,
218
+ "ENGINEERING_CHANGE_ORDERS": rng.randint(0, 14),
219
+ }
220
+
221
  if profile == "apparel_marketing":
222
  impressions = self._whole(rng, 25_000, 1_200_000, mult)
223
  clicks = self._bounded_child(rng, impressions, 0.012, 0.075)
 
725
  context_low = (context or "").lower()
726
  shipping_context = any(term in context_low for term in ("shipping", "shipment", "parcel", "freight", "delivery"))
727
  life_sciences_context = any(term in context_low for term in ("life sciences", "pharma", "medical device", "therapy", "biotech", "healthcare"))
728
+ medical_device_context = any(
729
+ term in context_low or term in (company_name or "").lower()
730
+ for term in (
731
+ "medical device",
732
+ "surgical",
733
+ "robotic",
734
+ "robotics",
735
+ "intuitive surgical",
736
+ "da vinci",
737
+ "ion endoluminal",
738
+ "manufacturing operations",
739
+ "inventory",
740
+ "warehouse",
741
+ )
742
+ )
743
+ higher_ed_context = any(
744
+ term in context_low or term in (company_name or "").lower()
745
+ for term in (
746
+ "university",
747
+ "college",
748
+ "higher education",
749
+ "student",
750
+ "students",
751
+ "enrollment",
752
+ "admissions",
753
+ "retention",
754
+ "campus",
755
+ "wake forest",
756
+ )
757
+ )
758
  service_values = (
759
  ["Ground", "Express", "Freight", "International Priority", "Last Mile", "Returns", "E-Commerce Fulfillment", "Healthcare Logistics"]
760
  if shipping_context
761
  else ["API Gateway", "Web App", "Data Platform", "Identity", "Payments", "Reporting", "Observability", "Collaboration"]
762
  )
763
+ if medical_device_context:
764
+ product_values = [
765
+ "da Vinci 5 Surgical System",
766
+ "da Vinci Xi Surgical System",
767
+ "Ion Endoluminal System",
768
+ "da Vinci SP Surgical System",
769
+ "EndoWrist Stapler",
770
+ "SureForm Stapler",
771
+ "Vessel Sealer Extend",
772
+ "SynchroSeal Instrument",
773
+ "Monopolar Curved Scissors",
774
+ "Maryland Bipolar Forceps",
775
+ "Fenestrated Bipolar Forceps",
776
+ "Large Needle Driver",
777
+ "ProGrasp Forceps",
778
+ "Tip-Up Fenestrated Grasper",
779
+ "Cadiere Forceps",
780
+ "Mega SutureCut Needle Driver",
781
+ "Harmonic ACE Curved Shears",
782
+ "Permanent Cautery Hook",
783
+ "Endoscope Plus",
784
+ "High Definition Vision Cart",
785
+ "Surgeon Console",
786
+ "Patient Cart",
787
+ "Integrated Table Motion Kit",
788
+ "Firefly Fluorescence Imaging Kit",
789
+ "Ion Vision Probe",
790
+ "Ion Flexision Biopsy Needle",
791
+ "Bronchoscopy Catheter Kit",
792
+ "Single-Port Access Kit",
793
+ "General Surgery Instrument Tray",
794
+ "Urology Procedure Kit",
795
+ "Gynecology Procedure Kit",
796
+ "Thoracic Procedure Kit",
797
+ "Colorectal Procedure Kit",
798
+ "Sterile Draping Kit",
799
+ "Instrument Reprocessing Tray",
800
+ "Service Parts Kit",
801
+ ]
802
+ elif life_sciences_context:
803
+ product_values = ["Oncology Therapy", "Immunology Therapy", "Surgical Device", "Orthopedic Implant", "Diagnostic Kit", "Cardiovascular Device", "Vision Care", "Consumer Health"]
804
+ else:
805
+ product_values = [f"{company} Core", f"{company} Plus", f"{company} Enterprise", f"{company} Analytics", f"{company} Services", f"{company} Mobile"]
806
+ region_values = (
807
+ ["North America", "Europe", "Japan", "Asia Pacific", "Latin America", "Field Service Network", "Manufacturing Sites", "Regulatory Hold"]
808
+ if medical_device_context
809
+ else ["North Carolina", "Southeast", "Mid-Atlantic", "Northeast", "Midwest", "West Coast", "International", "Online / Hybrid"]
810
+ if higher_ed_context
811
+ else ["North America", "EMEA", "APAC", "Latin America", "Public Sector", "Enterprise East", "Enterprise West", "Digital"]
812
+ )
813
+ warehouse_values = (
814
+ [
815
+ "Sunnyvale Final Assembly",
816
+ "Sunnyvale Service Depot",
817
+ "Mexicali Manufacturing Center",
818
+ "Auburn Hills Logistics Hub",
819
+ "Atlanta Field Service Depot",
820
+ "Dallas Distribution Center",
821
+ "Memphis Fulfillment Center",
822
+ "Raleigh Clinical Supply Hub",
823
+ "Boston Northeast Service Depot",
824
+ "Chicago Central Distribution Center",
825
+ "Phoenix Southwest Depot",
826
+ "Los Angeles West Coast Hub",
827
+ "Seattle Field Service Depot",
828
+ "Toronto Canada Distribution Center",
829
+ "Amsterdam EMEA Distribution Center",
830
+ "Freiburg Instrument Repair Center",
831
+ "Dublin Regulatory Hold Center",
832
+ "Singapore APAC Logistics Hub",
833
+ "Tokyo Japan Service Depot",
834
+ "Seoul Korea Distribution Center",
835
+ "Sydney Australia Service Depot",
836
+ "Shanghai Regional Logistics Hub",
837
+ "Sao Paulo Latin America Depot",
838
+ "Dubai Emerging Markets Hub",
839
+ ]
840
+ if medical_device_context
841
+ else ["Northeast DC", "Southeast DC", "Midwest DC", "Southwest DC", "West DC", "Import Hub"]
842
+ )
843
+ supplier_values = (
844
+ [
845
+ "Precision Motion Components",
846
+ "Endoscopic Optics Group",
847
+ "Sterile Packaging Partners",
848
+ "Robotic Arm Assemblies",
849
+ "Surgical Instrument Metals",
850
+ "Vision Sensor Technologies",
851
+ "Medical Cable Systems",
852
+ "Servo Motor Works",
853
+ "Printed Circuit Medical",
854
+ "Titanium Machining Partners",
855
+ "Clinical Cart Fabrication",
856
+ "Single-Use Components",
857
+ "Fluorescence Imaging Modules",
858
+ "Catheter Assembly Partners",
859
+ "Biopsy Needle Fabrication",
860
+ "Hospital Integration Hardware",
861
+ "Regulated Plastics Molding",
862
+ "Cleanroom Packaging Services",
863
+ "Optical Lens Manufacturing",
864
+ "Electromechanical Subsystems",
865
+ "Quality Inspection Services",
866
+ "Sterilization Services",
867
+ "Field Service Parts Supply",
868
+ "Global Freight Medical",
869
+ ]
870
+ if medical_device_context
871
+ else ["Preferred Supply", "Global Components", "Prime Source", "North Coast", "Vertex Manufacturing", "Reliable Partners"]
872
+ )
873
+ program_values = (
874
+ [
875
+ "School of Business",
876
+ "Computer Science",
877
+ "Health & Exercise Science",
878
+ "Data Analytics",
879
+ "Education",
880
+ "Engineering Sciences",
881
+ "Economics",
882
+ "Biology",
883
+ "Psychology",
884
+ "Communication",
885
+ "Politics & International Affairs",
886
+ "Accountancy",
887
+ "Finance",
888
+ "Biomedical Sciences",
889
+ "Law",
890
+ "Divinity",
891
+ "Sustainability",
892
+ "Liberal Studies",
893
+ ]
894
+ if higher_ed_context
895
+ else ["Business", "Computer Science", "Nursing", "Data Analytics", "Education", "Engineering"]
896
+ )
897
+ campus_values = (
898
+ [
899
+ "Reynolda Campus",
900
+ "Wake Downtown",
901
+ "Innovation Quarter",
902
+ "School of Business",
903
+ "School of Medicine",
904
+ "School of Law",
905
+ "Charlotte Center",
906
+ "Online Programs",
907
+ "Graduate School",
908
+ "Divinity School",
909
+ "Study Abroad",
910
+ "Athletics Cohort",
911
+ "Downtown Winston-Salem",
912
+ "Regional Partnerships",
913
+ "Executive Education",
914
+ "First-Year Experience",
915
+ "Pre-College Programs",
916
+ "Summer Session",
917
+ ]
918
+ if higher_ed_context
919
+ else None
920
+ )
921
+ student_segment_values = (
922
+ [
923
+ "First-Year Undergraduate",
924
+ "Transfer Students",
925
+ "North Carolina Residents",
926
+ "Out-of-State Domestic",
927
+ "International Students",
928
+ "First-Generation Students",
929
+ "Pell Eligible Students",
930
+ "Honors Applicants",
931
+ "Graduate Professional",
932
+ "Online Graduate",
933
+ "Adult Learners",
934
+ "Legacy Families",
935
+ "Student Athletes",
936
+ "STEM Applicants",
937
+ "Business Applicants",
938
+ "Health Sciences Applicants",
939
+ "Rural NC Applicants",
940
+ "Urban Market Applicants",
941
+ ]
942
+ if higher_ed_context
943
+ else None
944
+ )
945
+ term_values = (
946
+ ["Fall Early Decision I", "Fall Early Decision II", "Regular Decision", "Spring Transfer", "Summer Session", "Graduate Fall", "Graduate Spring", "Online Rolling"]
947
+ if higher_ed_context
948
+ else None
949
  )
950
  values = {
951
+ "REGIONS": region_values,
952
  "PRODUCTS": product_values,
953
  "CUSTOMERS": ["Enterprise", "Mid-Market", "SMB", "Strategic", "Commercial", "Public Sector"],
954
  "ACCOUNTS": ["Apex Systems", "Northstar Health", "Summit Retail", "Vertex Bank", "Beacon Logistics", "Cloudline Media"],
 
969
  "BRANDS": ["Luxury", "Premium", "Select Service", "Extended Stay", "Resort", "Convention", "Lifestyle", "Boutique"],
970
  "PRODUCT_CATEGORIES": ["Denim", "Outerwear", "Sportswear", "Dresses", "Footwear", "Accessories", "Kids", "Underwear"],
971
  "STORES": [f"{company} Flagship", f"{company} Core", f"{company} Outlet", f"{company} Digital", f"{company} Urban", f"{company} Suburban"],
972
+ "WAREHOUSES": warehouse_values,
973
+ "SUPPLIERS": supplier_values,
974
  "ROUTES": ["Northeast Corridor", "West Coast Loop", "Central Express", "Transatlantic", "Pacific Shuttle", "Southern Line"],
975
  "CARRIERS": ["Rapid Freight", "Prime Carrier", "ExpressLine", "Metro Delivery", "AirBridge", "Last Mile Co"],
976
  "AIRPORTS": ["JFK", "LAX", "ORD", "DFW", "ATL", "SFO"],
 
981
  "SERVICE_LINES": service_lines,
982
  "DEPARTMENTS": ["Engineering", "Sales", "Operations", "Finance", "Support", "Marketing"],
983
  "MATTERS": ["Contract Review", "Employment Claim", "Litigation", "Compliance Audit", "IP Filing", "Privacy Review"],
984
+ "PROGRAMS": program_values,
985
+ "CAMPUSES": campus_values or ["Campus 1", "Campus 2", "Campus 3", "Campus 4", "Campus 5", "Campus 6"],
986
+ "STUDENT_SEGMENTS": student_segment_values or ["Student Segment 1", "Student Segment 2", "Student Segment 3", "Student Segment 4", "Student Segment 5", "Student Segment 6"],
987
+ "TERMS": term_values or ["Term 1", "Term 2", "Term 3", "Term 4", "Term 5", "Term 6"],
988
  "TEAMS": context_values or [f"{company} Home Team", f"{company} Visiting Team", f"{company} Women's Team", f"{company} Development Team", f"{company} League Event", f"{company} Exhibition"],
989
  "VENUES": context_values or [company, f"{company} Arena", f"{company} Theater", f"{company} Premium Club", f"{company} Event Hall", f"{company} Fan Zone"],
990
  "EVENTS": context_values or [f"{company} Regular Season", f"{company} Premium Hospitality", f"{company} Group Sales Night", f"{company} Concert", f"{company} Theater Event", f"{company} Family Show"],
demoprep_app/pipeline/dataset_first.py CHANGED
@@ -21,8 +21,14 @@ class DatasetFirstBuild:
21
  ddl: str
22
 
23
 
24
- def infer_scenario_type(use_case: str, vertical: str | None = None, function: str | None = None) -> str | None:
25
- text = f"{vertical or ''} {function or ''} {use_case or ''}".lower()
 
 
 
 
 
 
26
  fn = (function or "").strip().lower()
27
 
28
  if fn == "hr":
@@ -72,6 +78,8 @@ def infer_scenario_type(use_case: str, vertical: str | None = None, function: st
72
  return "sales_pipeline"
73
  if any(tok in text for tok in ("warehousing", "warehouse", "supply chain", "fulfillment", "distribution center")):
74
  return "inventory_supply_chain"
 
 
75
  if any(tok in text for tok in ("automotive", "vehicle", "dealer", "dealership")) and "sales" in text:
76
  return "automotive_sales"
77
  if any(tok in text for tok in ("healthcare provider", "healthcare providers", "healthcare payer", "healthcare payers", "hospital", "clinic")) and "sales" in text:
@@ -101,7 +109,7 @@ def build_dataset_first_demo(
101
  llm_model: str | None = None,
102
  use_llm_contract: bool = False,
103
  ) -> DatasetFirstBuild | None:
104
- preliminary_type = infer_scenario_type(use_case, vertical, function)
105
  extraction = extract_dataset_scenario(
106
  company_name=company_name,
107
  company_url=company_url,
@@ -117,6 +125,7 @@ def build_dataset_first_demo(
117
  "retail_sales",
118
  "shipping_sales",
119
  "automotive_sales",
 
120
  "hotel_finance",
121
  "apparel_marketing",
122
  "sales_pipeline",
 
21
  ddl: str
22
 
23
 
24
+ def infer_scenario_type(
25
+ use_case: str,
26
+ vertical: str | None = None,
27
+ function: str | None = None,
28
+ company_name: str | None = None,
29
+ company_url: str | None = None,
30
+ ) -> str | None:
31
+ text = f"{company_name or ''} {company_url or ''} {vertical or ''} {function or ''} {use_case or ''}".lower()
32
  fn = (function or "").strip().lower()
33
 
34
  if fn == "hr":
 
78
  return "sales_pipeline"
79
  if any(tok in text for tok in ("warehousing", "warehouse", "supply chain", "fulfillment", "distribution center")):
80
  return "inventory_supply_chain"
81
+ if any(tok in text for tok in ("borgwarner", "automotive supplier", "auto supplier", "oem supplier", "tier 1", "tier-one", "component", "components", "powertrain", "drivetrain", "propulsion system")) and "sales" in text:
82
+ return "automotive_supplier_sales"
83
  if any(tok in text for tok in ("automotive", "vehicle", "dealer", "dealership")) and "sales" in text:
84
  return "automotive_sales"
85
  if any(tok in text for tok in ("healthcare provider", "healthcare providers", "healthcare payer", "healthcare payers", "hospital", "clinic")) and "sales" in text:
 
109
  llm_model: str | None = None,
110
  use_llm_contract: bool = False,
111
  ) -> DatasetFirstBuild | None:
112
+ preliminary_type = infer_scenario_type(use_case, vertical, function, company_name, company_url)
113
  extraction = extract_dataset_scenario(
114
  company_name=company_name,
115
  company_url=company_url,
 
125
  "retail_sales",
126
  "shipping_sales",
127
  "automotive_sales",
128
+ "automotive_supplier_sales",
129
  "hotel_finance",
130
  "apparel_marketing",
131
  "sales_pipeline",
demoprep_app/scenario/families.py CHANGED
@@ -192,6 +192,20 @@ SCENARIO_FAMILIES: dict[str, ScenarioFamilyTemplate] = {
192
  "How do retail, fleet, and lease channels compare?",
193
  ),
194
  ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  "logistics_route_operations": ScenarioFamilyTemplate(
196
  "logistics_route_operations",
197
  "LOGISTICS_ROUTES",
 
192
  "How do retail, fleet, and lease channels compare?",
193
  ),
194
  ),
195
+ "automotive_supplier_sales": ScenarioFamilyTemplate(
196
+ "automotive_supplier_sales",
197
+ "SUPPLIER_PROGRAM_SALES",
198
+ "oem-customer-component-program-month",
199
+ "AWARDED_REVENUE_USD",
200
+ ("OEM_CUSTOMERS", "COMPONENT_PROGRAMS", "VEHICLE_PLATFORMS", "PLANTS", "REGIONS"),
201
+ ("RFQ", "Quoted", "Nominated", "SOP", "Ramp", "Production", "End Of Production"),
202
+ "automotive_supplier_sales",
203
+ (
204
+ "Which OEM customers and component programs drive awarded revenue?",
205
+ "Where are tooling, warranty, or material costs pressuring margin?",
206
+ "Which vehicle platforms have the strongest backlog and quote conversion?",
207
+ ),
208
+ ),
209
  "logistics_route_operations": ScenarioFamilyTemplate(
210
  "logistics_route_operations",
211
  "LOGISTICS_ROUTES",
docs/thoughtspot_quality_reporting_source.md ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ThoughtSpot Quality Reporting Source
2
+
3
+ The DemoPrep quality reporting liveboard is:
4
+
5
+ - URL: `https://se-thoughtspot-cloud.thoughtspot.cloud/#/pinboard/85ccf9c9-2487-40b6-9f97-f9cf5207436b`
6
+ - Liveboard name: `demoprep testing`
7
+ - ThoughtSpot model: `demoprep_test_data`
8
+ - Model GUID: `db310fd7-b382-49d2-8398-b2f378d179f6`
9
+ - Physical table: Postgres `public.ts_quality_results`
10
+ - ThoughtSpot table GUID: `c12ee8a3-2b5d-4597-a873-eddb9ffaddc5`
11
+
12
+ Important: this reporting liveboard does **not** read from `session_logs`.
13
+ `session_logs` is useful for pipeline-stage diagnostics and run lineage, but deleting
14
+ bad rows from `session_logs` will not remove them from the quality reporting charts.
15
+
16
+ The liveboard's score charts use `ts_quality_results`, including:
17
+
18
+ - `Weekly Average Score Trend`: `average [Total Score] [Run Timestamp].weekly`
19
+ - `Weekly Average Data Score Trend`: `average [Data Score] [Run Timestamp].weekly`
20
+ - `Daily Data Score Trend`: `average [Data Score] [Run Timestamp].daily [Run Timestamp] = 'last 3 weeks'`
21
+
22
+ Cleanup workflow:
23
+
24
+ 1. Inspect the liveboard TML if the source is uncertain.
25
+ 2. Query `ts_quality_results` for the affected date window.
26
+ 3. Delete or filter rows in `ts_quality_results`, not only `session_logs`, when the
27
+ reporting liveboard needs to change.
28
+ 4. Use `session_logs` only to validate run provenance, user, stage completion, and
29
+ failure causes.
tests/e2e_quality.py CHANGED
@@ -44,6 +44,7 @@ load_dotenv(Path(__file__).parent.parent / ".env")
44
  BASE_URL = os.getenv("TEST_TARGET_URL", "") # may be overridden by --url flag at runtime
45
  TEST_USER = os.getenv("TEST_USER")
46
  TEST_PASSWORD = os.getenv("TEST_PASSWORD")
 
47
 
48
  CONFIG_FILE = Path(__file__).parent / "quality_config.yaml"
49
  RESULTS_DIR = Path(__file__).parent / "quality_results"
@@ -358,10 +359,61 @@ def _do_login(page: Page):
358
  page.fill('input[type=text]', TEST_USER)
359
  page.fill('input[type=password]', TEST_PASSWORD)
360
  page.click('button:has-text("Login")')
 
 
361
  page.wait_for_selector('button[role=tab]', timeout=90000)
362
  page.wait_for_timeout(3000)
363
 
364
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
  def _navigate_and_ensure_logged_in(page: Page, max_wait_secs: int = 300):
366
  """
367
  Navigate to BASE_URL and ensure we're on the logged-in app.
@@ -375,10 +427,7 @@ def _navigate_and_ensure_logged_in(page: Page, max_wait_secs: int = 300):
375
  try:
376
  page.goto(BASE_URL, timeout=90000)
377
  # Wait for either the logged-in app (tabs) or the login form
378
- page.wait_for_selector(
379
- 'button[role=tab], input[type=password]',
380
- timeout=60000,
381
- )
382
  break
383
  except Exception as nav_err:
384
  remaining = int(deadline - time.time())
@@ -391,11 +440,12 @@ def _navigate_and_ensure_logged_in(page: Page, max_wait_secs: int = 300):
391
 
392
  # If we landed on the login page (session expired or space rebuilt), re-login
393
  try:
394
- if page.locator('input[type=password]').is_visible(timeout=2000):
395
  print(" 🔑 Session expired — re-logging in...")
396
  _do_login(page)
397
  except Exception:
398
  pass # Already on the app — no login needed
 
399
 
400
 
401
  def submit_job(page: Page, test_case: dict):
@@ -1653,6 +1703,30 @@ def save_summary_md(run: dict, json_path: Path, env_name: str = "") -> Path:
1653
  return md_path
1654
 
1655
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1656
  # ---------------------------------------------------------------------------
1657
  # Main
1658
  # ---------------------------------------------------------------------------
@@ -1664,9 +1738,11 @@ def run_quality_suite(max_tests: int = None, env_name: str = "", suite_override:
1664
  suite = suite_override or build_test_suite(config)
1665
  if max_tests:
1666
  suite = suite[:max_tests]
 
1667
 
1668
  print(f"\n{'='*62}")
1669
  print(f" DemoPrep Quality Run — {datetime.now().strftime('%Y-%m-%d %H:%M')}")
 
1670
  print(f" Target: {BASE_URL}")
1671
  print(f" {len(suite)} tests | "
1672
  f"{sum(1 for t in suite if t['type']=='fixed')} fixed "
@@ -1679,7 +1755,6 @@ def run_quality_suite(max_tests: int = None, env_name: str = "", suite_override:
1679
  label = {"fixed": "🔒", "random": "🎲", "ai_generated": "🤖", "custom": "✏️"}[tc["type"]]
1680
  print(f" [{i}] {label} {tc['name']}")
1681
 
1682
- run_id = str(uuid.uuid4())[:8]
1683
  results = []
1684
 
1685
  with sync_playwright() as p:
@@ -1744,7 +1819,8 @@ def run_quality_suite(max_tests: int = None, env_name: str = "", suite_override:
1744
  "test_count": len(results), "tests": results,
1745
  }
1746
  path = save_results(run)
1747
- save_summary_md(run, path, env_name=env_name)
 
1748
 
1749
  # --- Summary table ---
1750
  try:
 
44
  BASE_URL = os.getenv("TEST_TARGET_URL", "") # may be overridden by --url flag at runtime
45
  TEST_USER = os.getenv("TEST_USER")
46
  TEST_PASSWORD = os.getenv("TEST_PASSWORD")
47
+ TEST_NEW_PASSWORD = os.getenv("TEST_NEW_PASSWORD", "")
48
 
49
  CONFIG_FILE = Path(__file__).parent / "quality_config.yaml"
50
  RESULTS_DIR = Path(__file__).parent / "quality_results"
 
359
  page.fill('input[type=text]', TEST_USER)
360
  page.fill('input[type=password]', TEST_PASSWORD)
361
  page.click('button:has-text("Login")')
362
+ _wait_for_visible_app_or_auth_control(page, include_login=False, timeout=90000)
363
+ _handle_forced_password_change(page)
364
  page.wait_for_selector('button[role=tab]', timeout=90000)
365
  page.wait_for_timeout(3000)
366
 
367
 
368
+ def _wait_for_visible_app_or_auth_control(page: Page, *, include_login: bool, timeout: int):
369
+ """Wait until a visible app tab or auth control is present."""
370
+ page.wait_for_function(
371
+ """
372
+ ({ includeLogin }) => {
373
+ const visible = (el) => !!(
374
+ el &&
375
+ (el.offsetWidth || el.offsetHeight || el.getClientRects().length)
376
+ );
377
+ const hasVisibleTab = Array.from(document.querySelectorAll('button[role="tab"]'))
378
+ .some(visible);
379
+ if (hasVisibleTab) return true;
380
+
381
+ const buttons = Array.from(document.querySelectorAll('button'))
382
+ .filter(visible)
383
+ .map((button) => (button.textContent || '').trim());
384
+ if (buttons.some((text) => text.includes('Change Password'))) return true;
385
+ if (includeLogin && buttons.some((text) => text.includes('Login'))) return true;
386
+ return false;
387
+ }
388
+ """,
389
+ arg={"includeLogin": include_login},
390
+ timeout=timeout,
391
+ )
392
+
393
+
394
+ def _handle_forced_password_change(page: Page):
395
+ """Handle or explicitly fail on the app's temporary-password gate."""
396
+ try:
397
+ gate = page.get_by_text("Change Password Required", exact=False)
398
+ if not gate.is_visible(timeout=1500):
399
+ return
400
+ except Exception:
401
+ return
402
+
403
+ if not TEST_NEW_PASSWORD:
404
+ raise RuntimeError(
405
+ "Test user is blocked by the temporary-password gate. "
406
+ "Clear must_change_password for TEST_USER or set TEST_NEW_PASSWORD "
407
+ "so the harness can complete the required password change."
408
+ )
409
+
410
+ page.locator('input[placeholder="Enter the password you just used to sign in"]').first.fill(TEST_PASSWORD)
411
+ page.locator('input[placeholder="At least 8 characters"]').first.fill(TEST_NEW_PASSWORD)
412
+ page.locator('input[placeholder="Repeat new password"]').first.fill(TEST_NEW_PASSWORD)
413
+ page.click('button:has-text("Change Password")', timeout=5000)
414
+ page.wait_for_selector('button[role=tab]', timeout=30000)
415
+
416
+
417
  def _navigate_and_ensure_logged_in(page: Page, max_wait_secs: int = 300):
418
  """
419
  Navigate to BASE_URL and ensure we're on the logged-in app.
 
427
  try:
428
  page.goto(BASE_URL, timeout=90000)
429
  # Wait for either the logged-in app (tabs) or the login form
430
+ _wait_for_visible_app_or_auth_control(page, include_login=True, timeout=60000)
 
 
 
431
  break
432
  except Exception as nav_err:
433
  remaining = int(deadline - time.time())
 
440
 
441
  # If we landed on the login page (session expired or space rebuilt), re-login
442
  try:
443
+ if page.locator('button:has-text("Login")').is_visible(timeout=2000):
444
  print(" 🔑 Session expired — re-logging in...")
445
  _do_login(page)
446
  except Exception:
447
  pass # Already on the app — no login needed
448
+ _handle_forced_password_change(page)
449
 
450
 
451
  def submit_job(page: Page, test_case: dict):
 
1703
  return md_path
1704
 
1705
 
1706
+ def print_handoff_block(run: dict, json_path: Path, md_path: Path, env_name: str = ""):
1707
+ latest_name = f"latest_{env_name}_summary.md" if env_name else "latest_summary.md"
1708
+ latest_path = RESULTS_DIR / latest_name
1709
+ timestamp = run.get("timestamp", "")
1710
+ run_id = run.get("run_id", "")
1711
+ target = run.get("target_url", "")
1712
+ avg = run.get("avg_score", "")
1713
+ grade = run.get("overall_grade", "")
1714
+
1715
+ print("\n📌 Agent handoff")
1716
+ print(f" Run ID: {run_id}")
1717
+ print(f" Timestamp: {timestamp}")
1718
+ print(f" Target: {target}")
1719
+ print(f" Results: {json_path}")
1720
+ print(f" Summary: {md_path}")
1721
+ print(f" Latest: {latest_path}")
1722
+ print(
1723
+ " Paste this: "
1724
+ f"DemoPrep quality run {run_id} ({timestamp}) "
1725
+ f"avg={avg}/{grade} target={target} "
1726
+ f"results={json_path} summary={md_path}"
1727
+ )
1728
+
1729
+
1730
  # ---------------------------------------------------------------------------
1731
  # Main
1732
  # ---------------------------------------------------------------------------
 
1738
  suite = suite_override or build_test_suite(config)
1739
  if max_tests:
1740
  suite = suite[:max_tests]
1741
+ run_id = str(uuid.uuid4())[:8]
1742
 
1743
  print(f"\n{'='*62}")
1744
  print(f" DemoPrep Quality Run — {datetime.now().strftime('%Y-%m-%d %H:%M')}")
1745
+ print(f" Run ID: {run_id}")
1746
  print(f" Target: {BASE_URL}")
1747
  print(f" {len(suite)} tests | "
1748
  f"{sum(1 for t in suite if t['type']=='fixed')} fixed "
 
1755
  label = {"fixed": "🔒", "random": "🎲", "ai_generated": "🤖", "custom": "✏️"}[tc["type"]]
1756
  print(f" [{i}] {label} {tc['name']}")
1757
 
 
1758
  results = []
1759
 
1760
  with sync_playwright() as p:
 
1819
  "test_count": len(results), "tests": results,
1820
  }
1821
  path = save_results(run)
1822
+ md_path = save_summary_md(run, path, env_name=env_name)
1823
+ print_handoff_block(run, path, md_path, env_name=env_name)
1824
 
1825
  # --- Summary table ---
1826
  try:
tests/test_dataset_first_builders.py CHANGED
@@ -259,6 +259,51 @@ def test_grocery_finance_routes_to_cpg_financials_not_sales_fact():
259
  assert "LTV_USD" not in fact_columns
260
 
261
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  def test_function_specific_routing_overrides_industry_terms():
263
  cases = [
264
  ("Financial Services", "Banking", "HR", "workforce_hr"),
@@ -306,6 +351,71 @@ def test_it_operations_has_two_year_hourly_calendar():
306
  assert max(row["INCIDENTS"] for row in fact.rows[:100]) <= 10
307
 
308
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  def test_new_quality_pool_domains_are_not_generic_finance_or_pipeline():
310
  cases = [
311
  ("J.B. Hunt", "jbhunt.com", "Transportation & Logistics", "Trucking Finance", "Finance", "trucking_finance", "TRUCKING_FINANCIALS", {"LOADS", "REVENUE_PER_LOADED_MILE_USD", "EMPTY_MILE_PCT"}),
 
259
  assert "LTV_USD" not in fact_columns
260
 
261
 
262
+ def test_automotive_supplier_sales_routes_to_supplier_programs():
263
+ build = build_dataset_first_demo(
264
+ company_name="BorgWarner",
265
+ company_url="https://borgwarner.com",
266
+ use_case="Automotive Sales",
267
+ vertical="Manufacturing",
268
+ function="Sales",
269
+ row_count_guidance=100,
270
+ )
271
+
272
+ assert build is not None
273
+ assert build.scenario.scenario_type == "automotive_supplier_sales"
274
+ table_map = build.dataset.table_map()
275
+ assert "SUPPLIER_PROGRAM_SALES" in table_map
276
+ assert "VEHICLE_SALES" not in table_map
277
+ fact_columns = {column.name for column in table_map["SUPPLIER_PROGRAM_SALES"].columns}
278
+ assert {
279
+ "RFQS_RECEIVED",
280
+ "QUOTED_REVENUE_USD",
281
+ "AWARDED_REVENUE_USD",
282
+ "BACKLOG_USD",
283
+ "TOOLING_RECOVERY_USD",
284
+ "WARRANTY_RESERVE_USD",
285
+ "PROGRAM_MARGIN_USD",
286
+ "DEFECT_PPM",
287
+ } <= fact_columns
288
+ assert "DEALER_HOLDBACK_USD" not in fact_columns
289
+ assert "TEST_DRIVES" not in fact_columns
290
+
291
+
292
+ def test_vehicle_oem_sales_stays_on_automotive_sales():
293
+ build = build_dataset_first_demo(
294
+ company_name="Stellantis",
295
+ company_url="https://stellantis.com",
296
+ use_case="Automotive Sales",
297
+ vertical="Manufacturing",
298
+ function="Sales",
299
+ row_count_guidance=100,
300
+ )
301
+
302
+ assert build is not None
303
+ assert build.scenario.scenario_type == "automotive_sales"
304
+ assert "VEHICLE_SALES" in build.dataset.table_map()
305
+
306
+
307
  def test_function_specific_routing_overrides_industry_terms():
308
  cases = [
309
  ("Financial Services", "Banking", "HR", "workforce_hr"),
 
351
  assert max(row["INCIDENTS"] for row in fact.rows[:100]) <= 10
352
 
353
 
354
+ def test_medical_device_inventory_dimensions_are_not_numbered_fillers():
355
+ build = build_dataset_first_demo(
356
+ company_name="Intuitive Surgical",
357
+ company_url="https://www.intuitive.com",
358
+ use_case="Medical device manufacturing operations inventory and warehouse supply chain",
359
+ vertical="Manufacturing",
360
+ function="Operations",
361
+ row_count_guidance=100,
362
+ )
363
+
364
+ assert build is not None
365
+ assert build.scenario.scenario_type == "inventory_supply_chain"
366
+
367
+ table_map = build.dataset.table_map()
368
+ products = [row["PRODUCT_NAME"] for row in table_map["PRODUCTS"].rows]
369
+ warehouses = [row["WAREHOUSE_NAME"] for row in table_map["WAREHOUSES"].rows]
370
+ suppliers = [row["SUPPLIER_NAME"] for row in table_map["SUPPLIERS"].rows]
371
+ regions = [row["REGION_NAME"] for row in table_map["REGIONS"].rows]
372
+
373
+ assert len(products) == 36
374
+ assert len(warehouses) == 24
375
+ assert len(suppliers) == 24
376
+ assert "da Vinci 5 Surgical System" in products
377
+ assert "Sunnyvale Final Assembly" in warehouses
378
+ assert "Precision Motion Components" in suppliers
379
+ assert "Manufacturing Sites" in regions
380
+
381
+ generated_names = products + warehouses + suppliers
382
+ assert not any("Product 29" in name or "Warehouse 21" in name or "Supplier 19" in name for name in generated_names)
383
+ assert not any(name.rsplit(" ", 1)[-1].isdigit() for name in generated_names)
384
+
385
+
386
+ def test_higher_education_enrollment_dimensions_are_not_numbered_fillers():
387
+ build = build_dataset_first_demo(
388
+ company_name="Wake Forest University",
389
+ company_url="https://www.wfu.edu",
390
+ use_case="Student Success Enrollment",
391
+ vertical="Education",
392
+ function="Operations",
393
+ row_count_guidance=100,
394
+ )
395
+
396
+ assert build is not None
397
+ assert build.scenario.scenario_type == "education_enrollment"
398
+
399
+ table_map = build.dataset.table_map()
400
+ programs = [row["PROGRAM_NAME"] for row in table_map["PROGRAMS"].rows]
401
+ campuses = [row["CAMPUS_NAME"] for row in table_map["CAMPUSES"].rows]
402
+ segments = [row["STUDENT_SEGMENT_NAME"] for row in table_map["STUDENT_SEGMENTS"].rows]
403
+ terms = [row["TERM_NAME"] for row in table_map["TERMS"].rows]
404
+
405
+ assert len(programs) == 18
406
+ assert len(campuses) == 18
407
+ assert len(segments) == 18
408
+ assert len(terms) == 8
409
+ assert "School of Business" in programs
410
+ assert "Reynolda Campus" in campuses
411
+ assert "First-Generation Students" in segments
412
+ assert "Fall Early Decision I" in terms
413
+
414
+ generated_names = programs + campuses + segments + terms
415
+ assert not any("Program 17" in name or "Campus 18" in name or "Student Segment 14" in name or "Term 07" in name for name in generated_names)
416
+ assert not any(name.rsplit(" ", 1)[-1].isdigit() for name in generated_names)
417
+
418
+
419
  def test_new_quality_pool_domains_are_not_generic_finance_or_pipeline():
420
  cases = [
421
  ("J.B. Hunt", "jbhunt.com", "Transportation & Logistics", "Trucking Finance", "Finance", "trucking_finance", "TRUCKING_FINANCIALS", {"LOADS", "REVENUE_PER_LOADED_MILE_USD", "EMPTY_MILE_PCT"}),