mango-lollipop 0.1.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.
@@ -0,0 +1,120 @@
1
+ export type Channel = "email" | "sms" | "in-app" | "push";
2
+ export type AARRRStage = "AQ" | "AC" | "RV" | "RT" | "RF";
3
+ export interface Trigger {
4
+ event: string;
5
+ type: "event" | "scheduled" | "behavioral";
6
+ schedule?: string;
7
+ }
8
+ export interface Guard {
9
+ condition: string;
10
+ expression: string;
11
+ }
12
+ export interface Suppression {
13
+ condition: string;
14
+ expression: string;
15
+ }
16
+ export interface CTA {
17
+ text: string;
18
+ url?: string;
19
+ }
20
+ export interface Message {
21
+ id: string;
22
+ stage: AARRRStage | "TX";
23
+ name: string;
24
+ classification: "transactional" | "lifecycle";
25
+ trigger: Trigger;
26
+ wait: string;
27
+ guards: Guard[];
28
+ suppressions: Suppression[];
29
+ subject: string;
30
+ preheader?: string;
31
+ body: string;
32
+ cta: CTA;
33
+ channel: Channel;
34
+ format: "plain" | "rich";
35
+ from: string;
36
+ segment: string;
37
+ tags: string[];
38
+ goal: string;
39
+ comments: string;
40
+ }
41
+ export interface SenderPersona {
42
+ name: string;
43
+ role: string;
44
+ use_for: string[];
45
+ }
46
+ export interface VoiceProfile {
47
+ tone: string;
48
+ formality: number;
49
+ emoji_usage: "none" | "light" | "heavy";
50
+ signature_style: string;
51
+ sample_phrases: string[];
52
+ sender_personas: SenderPersona[];
53
+ }
54
+ export interface EventTaxonomy {
55
+ identity: string[];
56
+ activation: string[];
57
+ engagement: string[];
58
+ conversion: string[];
59
+ retention: string[];
60
+ }
61
+ export interface AnalysisCompany {
62
+ name: string;
63
+ product_type: string;
64
+ target_audience: string;
65
+ key_value_prop: string;
66
+ aha_moment: string;
67
+ key_features: string[];
68
+ pricing_model: string;
69
+ }
70
+ export interface AnalysisTags {
71
+ sources: string[];
72
+ plans: string[];
73
+ segments: string[];
74
+ features: string[];
75
+ }
76
+ export interface ExistingPerformance {
77
+ open_rate_avg: string;
78
+ click_rate_avg: string;
79
+ problem_areas: string[];
80
+ }
81
+ export interface ExistingMessaging {
82
+ messages_count: number;
83
+ stages_covered: (AARRRStage | "TX")[];
84
+ stages_missing: (AARRRStage | "TX")[];
85
+ channels_used: Channel[];
86
+ performance: ExistingPerformance;
87
+ primary_goal: string;
88
+ messages: unknown[];
89
+ }
90
+ export interface Analysis {
91
+ path: "fresh" | "existing";
92
+ company: AnalysisCompany;
93
+ channels: Channel[];
94
+ voice: VoiceProfile;
95
+ events: EventTaxonomy;
96
+ tags: AnalysisTags;
97
+ existing?: ExistingMessaging;
98
+ recommendations: string[];
99
+ }
100
+ export interface ProjectConfig {
101
+ name: string;
102
+ version: string;
103
+ created: string;
104
+ stage: string;
105
+ path: "fresh" | "existing" | null;
106
+ channels: Channel[];
107
+ analysis: Analysis | null;
108
+ matrix: {
109
+ messages: Message[];
110
+ } | null;
111
+ }
112
+ export declare function isValidChannel(ch: unknown): ch is Channel;
113
+ export declare function isValidStage(stage: unknown): stage is AARRRStage;
114
+ export declare function isValidWaitDuration(wait: unknown): boolean;
115
+ export interface ValidationResult {
116
+ valid: boolean;
117
+ errors: string[];
118
+ }
119
+ export declare function validateMessage(msg: unknown): ValidationResult;
120
+ export declare function validateAnalysis(analysis: unknown): ValidationResult;
package/dist/schema.js ADDED
@@ -0,0 +1,211 @@
1
+ // =============================================================================
2
+ // Mango Lollipop — TypeScript Schema & Validation
3
+ // =============================================================================
4
+ // -----------------------------------------------------------------------------
5
+ // Constants
6
+ // -----------------------------------------------------------------------------
7
+ const VALID_CHANNELS = ["email", "sms", "in-app", "push"];
8
+ const VALID_STAGES = ["AQ", "AC", "RV", "RT", "RF", "TX"];
9
+ const VALID_AARRR_STAGES = ["AQ", "AC", "RV", "RT", "RF"];
10
+ // ISO 8601 duration pattern: P[nY][nM][nD][T[nH][nM][nS]] or PnW
11
+ const ISO_8601_DURATION_RE = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$|^P(\d+)W$/;
12
+ // -----------------------------------------------------------------------------
13
+ // Type Guards
14
+ // -----------------------------------------------------------------------------
15
+ export function isValidChannel(ch) {
16
+ return typeof ch === "string" && VALID_CHANNELS.includes(ch);
17
+ }
18
+ export function isValidStage(stage) {
19
+ return typeof stage === "string" && VALID_AARRR_STAGES.includes(stage);
20
+ }
21
+ // -----------------------------------------------------------------------------
22
+ // Validation Helpers
23
+ // -----------------------------------------------------------------------------
24
+ export function isValidWaitDuration(wait) {
25
+ if (typeof wait !== "string")
26
+ return false;
27
+ return ISO_8601_DURATION_RE.test(wait);
28
+ }
29
+ export function validateMessage(msg) {
30
+ const errors = [];
31
+ if (typeof msg !== "object" || msg === null) {
32
+ return { valid: false, errors: ["Message must be a non-null object"] };
33
+ }
34
+ const m = msg;
35
+ // Required string fields
36
+ const requiredStrings = [
37
+ ["id", "id"],
38
+ ["name", "name"],
39
+ ["subject", "subject"],
40
+ ["body", "body"],
41
+ ["from", "from"],
42
+ ["segment", "segment"],
43
+ ["goal", "goal"],
44
+ ["wait", "wait"],
45
+ ];
46
+ for (const [field, label] of requiredStrings) {
47
+ if (typeof m[field] !== "string" || m[field].length === 0) {
48
+ errors.push(`Missing or empty required field: ${label}`);
49
+ }
50
+ }
51
+ // Stage
52
+ const validStagesAll = VALID_STAGES;
53
+ if (typeof m.stage !== "string" || !validStagesAll.includes(m.stage)) {
54
+ errors.push(`Invalid stage: "${String(m.stage)}". Must be one of: ${validStagesAll.join(", ")}`);
55
+ }
56
+ // Classification
57
+ if (m.classification !== "transactional" && m.classification !== "lifecycle") {
58
+ errors.push(`Invalid classification: "${String(m.classification)}". Must be "transactional" or "lifecycle"`);
59
+ }
60
+ // Wait duration
61
+ if (typeof m.wait === "string" && !isValidWaitDuration(m.wait)) {
62
+ errors.push(`Invalid wait duration: "${m.wait}". Must be ISO 8601 duration (e.g. "P0D", "PT5M", "P2D")`);
63
+ }
64
+ // Format
65
+ if (m.format !== "plain" && m.format !== "rich") {
66
+ errors.push(`Invalid format: "${String(m.format)}". Must be "plain" or "rich"`);
67
+ }
68
+ // Channel (singular)
69
+ if (!isValidChannel(m.channel)) {
70
+ errors.push(`Invalid channel: "${String(m.channel)}". Must be one of: ${VALID_CHANNELS.join(", ")}`);
71
+ }
72
+ // Tags
73
+ if (!Array.isArray(m.tags)) {
74
+ errors.push("tags must be an array");
75
+ }
76
+ // Guards
77
+ if (!Array.isArray(m.guards)) {
78
+ errors.push("guards must be an array");
79
+ }
80
+ // Suppressions
81
+ if (!Array.isArray(m.suppressions)) {
82
+ errors.push("suppressions must be an array");
83
+ }
84
+ // Trigger
85
+ if (typeof m.trigger !== "object" || m.trigger === null) {
86
+ errors.push("trigger must be a non-null object");
87
+ }
88
+ else {
89
+ const t = m.trigger;
90
+ if (typeof t.event !== "string" || t.event.length === 0) {
91
+ errors.push("trigger.event is required");
92
+ }
93
+ if (t.type !== "event" && t.type !== "scheduled" && t.type !== "behavioral") {
94
+ errors.push(`Invalid trigger.type: "${String(t.type)}". Must be "event", "scheduled", or "behavioral"`);
95
+ }
96
+ }
97
+ // CTA
98
+ if (typeof m.cta !== "object" || m.cta === null) {
99
+ errors.push("cta must be a non-null object");
100
+ }
101
+ else {
102
+ const c = m.cta;
103
+ if (typeof c.text !== "string" || c.text.length === 0) {
104
+ errors.push("cta.text is required");
105
+ }
106
+ }
107
+ return { valid: errors.length === 0, errors };
108
+ }
109
+ export function validateAnalysis(analysis) {
110
+ const errors = [];
111
+ if (typeof analysis !== "object" || analysis === null) {
112
+ return { valid: false, errors: ["Analysis must be a non-null object"] };
113
+ }
114
+ const a = analysis;
115
+ // Path
116
+ if (a.path !== "fresh" && a.path !== "existing") {
117
+ errors.push(`Invalid path: "${String(a.path)}". Must be "fresh" or "existing"`);
118
+ }
119
+ // Company
120
+ if (typeof a.company !== "object" || a.company === null) {
121
+ errors.push("company is required");
122
+ }
123
+ else {
124
+ const c = a.company;
125
+ const companyFields = ["name", "product_type", "target_audience", "key_value_prop", "aha_moment", "pricing_model"];
126
+ for (const field of companyFields) {
127
+ if (typeof c[field] !== "string" || c[field].length === 0) {
128
+ errors.push(`Missing or empty company.${field}`);
129
+ }
130
+ }
131
+ if (!Array.isArray(c.key_features) || c.key_features.length === 0) {
132
+ errors.push("company.key_features must be a non-empty array");
133
+ }
134
+ }
135
+ // Channels
136
+ if (!Array.isArray(a.channels) || a.channels.length === 0) {
137
+ errors.push("Must have at least one channel");
138
+ }
139
+ else {
140
+ for (const ch of a.channels) {
141
+ if (!isValidChannel(ch)) {
142
+ errors.push(`Invalid channel: "${String(ch)}". Must be one of: ${VALID_CHANNELS.join(", ")}`);
143
+ }
144
+ }
145
+ }
146
+ // Voice
147
+ if (typeof a.voice !== "object" || a.voice === null) {
148
+ errors.push("voice profile is required");
149
+ }
150
+ else {
151
+ const v = a.voice;
152
+ if (typeof v.tone !== "string" || v.tone.length === 0) {
153
+ errors.push("voice.tone is required");
154
+ }
155
+ if (typeof v.formality !== "number" || v.formality < 1 || v.formality > 5) {
156
+ errors.push("voice.formality must be a number between 1 and 5");
157
+ }
158
+ if (v.emoji_usage !== "none" && v.emoji_usage !== "light" && v.emoji_usage !== "heavy") {
159
+ errors.push(`Invalid voice.emoji_usage: "${String(v.emoji_usage)}". Must be "none", "light", or "heavy"`);
160
+ }
161
+ if (!Array.isArray(v.sample_phrases)) {
162
+ errors.push("voice.sample_phrases must be an array");
163
+ }
164
+ if (!Array.isArray(v.sender_personas)) {
165
+ errors.push("voice.sender_personas must be an array");
166
+ }
167
+ }
168
+ // Events
169
+ if (typeof a.events !== "object" || a.events === null) {
170
+ errors.push("events taxonomy is required");
171
+ }
172
+ else {
173
+ const e = a.events;
174
+ const eventCategories = ["identity", "activation", "engagement", "conversion", "retention"];
175
+ for (const cat of eventCategories) {
176
+ if (!Array.isArray(e[cat])) {
177
+ errors.push(`events.${cat} must be an array`);
178
+ }
179
+ }
180
+ }
181
+ // Tags
182
+ if (typeof a.tags !== "object" || a.tags === null) {
183
+ errors.push("tags is required");
184
+ }
185
+ // Recommendations
186
+ if (!Array.isArray(a.recommendations)) {
187
+ errors.push("recommendations must be an array");
188
+ }
189
+ // PATH B: existing (only validated if path is "existing")
190
+ if (a.path === "existing") {
191
+ if (typeof a.existing !== "object" || a.existing === null) {
192
+ errors.push('existing messaging data is required when path is "existing"');
193
+ }
194
+ else {
195
+ const ex = a.existing;
196
+ if (typeof ex.messages_count !== "number") {
197
+ errors.push("existing.messages_count must be a number");
198
+ }
199
+ if (!Array.isArray(ex.stages_covered)) {
200
+ errors.push("existing.stages_covered must be an array");
201
+ }
202
+ if (!Array.isArray(ex.stages_missing)) {
203
+ errors.push("existing.stages_missing must be an array");
204
+ }
205
+ if (typeof ex.primary_goal !== "string") {
206
+ errors.push("existing.primary_goal is required");
207
+ }
208
+ }
209
+ }
210
+ return { valid: errors.length === 0, errors };
211
+ }