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.
- package/CLAUDE.md +69 -0
- package/LICENSE +21 -0
- package/README.md +264 -0
- package/bin/mango-lollipop.js +385 -0
- package/dist/excel.d.ts +4 -0
- package/dist/excel.js +342 -0
- package/dist/html.d.ts +4 -0
- package/dist/html.js +938 -0
- package/dist/schema.d.ts +120 -0
- package/dist/schema.js +211 -0
- package/lib/excel.ts +433 -0
- package/lib/html.ts +993 -0
- package/lib/schema.ts +394 -0
- package/package.json +44 -0
- package/skills/audit/SKILL.md +248 -0
- package/skills/dev-handoff/SKILL.md +295 -0
- package/skills/generate-dashboard/SKILL.md +195 -0
- package/skills/generate-matrix/SKILL.md +374 -0
- package/skills/generate-messages/SKILL.md +262 -0
- package/skills/iterate/SKILL.md +242 -0
- package/skills/start/SKILL.md +310 -0
- package/templates/copywriting-guide.md +155 -0
- package/templates/dashboard.html +522 -0
- package/templates/events/saas-collaboration.yaml +50 -0
- package/templates/events/saas-document.yaml +44 -0
- package/templates/events/saas-general.yaml +38 -0
- package/templates/events/saas-marketplace.yaml +48 -0
- package/templates/overview.html +598 -0
- package/templates/saas-matrix.json +172 -0
package/dist/schema.d.ts
ADDED
|
@@ -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
|
+
}
|