gasp-standard-mcp 1.0.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/LICENSE +21 -0
- package/README.md +61 -0
- package/build/export-data.d.ts +8 -0
- package/build/export-data.js +32 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +250 -0
- package/build/loader.d.ts +31 -0
- package/build/loader.js +163 -0
- package/build/parser.d.ts +11 -0
- package/build/parser.js +256 -0
- package/build/standard-data.json +1 -0
- package/build/standard-version.json +1 -0
- package/build/types.d.ts +63 -0
- package/build/types.js +1 -0
- package/package.json +51 -0
package/build/parser.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/** Map of file basenames to department display names */
|
|
2
|
+
const DEPARTMENT_NAMES = {
|
|
3
|
+
core: "Core",
|
|
4
|
+
sales: "Sales",
|
|
5
|
+
marketing: "Marketing",
|
|
6
|
+
"customer-success": "Customer Success",
|
|
7
|
+
support: "Support",
|
|
8
|
+
product: "Product",
|
|
9
|
+
engineering: "Engineering",
|
|
10
|
+
finance: "Finance",
|
|
11
|
+
onboarding: "Onboarding",
|
|
12
|
+
people: "People",
|
|
13
|
+
revops: "RevOps",
|
|
14
|
+
partnerships: "Partnerships",
|
|
15
|
+
"professional-services": "Professional Services",
|
|
16
|
+
};
|
|
17
|
+
/** Known abbreviation → full name mapping */
|
|
18
|
+
const ABBREVIATION_MAP = {
|
|
19
|
+
MRR: ["Monthly Recurring Revenue"],
|
|
20
|
+
ARR: ["Annual Recurring Revenue"],
|
|
21
|
+
NRR: ["Net Revenue Retention", "NDR", "Net Dollar Retention"],
|
|
22
|
+
NDR: ["Net Dollar Retention", "NRR", "Net Revenue Retention"],
|
|
23
|
+
GRR: ["Gross Revenue Retention"],
|
|
24
|
+
CAC: ["Customer Acquisition Cost"],
|
|
25
|
+
LTV: ["Customer Lifetime Value", "CLV", "CLTV"],
|
|
26
|
+
CLV: ["Customer Lifetime Value", "LTV", "CLTV"],
|
|
27
|
+
CLTV: ["Customer Lifetime Value", "LTV", "CLV"],
|
|
28
|
+
NPS: ["Net Promoter Score"],
|
|
29
|
+
CSAT: ["Customer Satisfaction Score"],
|
|
30
|
+
CES: ["Customer Effort Score"],
|
|
31
|
+
ARPA: ["Average Revenue Per Account"],
|
|
32
|
+
ACV: ["Annual Contract Value"],
|
|
33
|
+
TTFV: ["Time to First Value"],
|
|
34
|
+
FRT: ["First Response Time"],
|
|
35
|
+
MTTR: ["Mean Time to Resolve", "Mean Time to Recovery"],
|
|
36
|
+
DAU: ["Daily Active Users"],
|
|
37
|
+
MAU: ["Monthly Active Users"],
|
|
38
|
+
WAU: ["Weekly Active Users"],
|
|
39
|
+
MQL: ["Marketing Qualified Lead"],
|
|
40
|
+
SQL: ["Sales Qualified Lead"],
|
|
41
|
+
DSO: ["Days Sales Outstanding"],
|
|
42
|
+
EBITDA: ["Earnings Before Interest Taxes Depreciation and Amortization"],
|
|
43
|
+
COGS: ["Cost of Goods Sold"],
|
|
44
|
+
DORA: ["DevOps Research and Assessment"],
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Extract abbreviation from metric name heading.
|
|
48
|
+
* e.g. "MRR (Monthly Recurring Revenue)" → ["MRR"]
|
|
49
|
+
* e.g. "Net Revenue Retention (NRR)" → ["NRR", "NDR"]
|
|
50
|
+
*/
|
|
51
|
+
function extractAliases(name) {
|
|
52
|
+
const aliases = [];
|
|
53
|
+
// Match abbreviation at start: "MRR (Monthly Recurring Revenue)"
|
|
54
|
+
const leadingMatch = name.match(/^([A-Z]{2,})\s/);
|
|
55
|
+
if (leadingMatch) {
|
|
56
|
+
aliases.push(leadingMatch[1]);
|
|
57
|
+
}
|
|
58
|
+
// Match abbreviation in parens: "Net Revenue Retention (NRR)"
|
|
59
|
+
const parenMatch = name.match(/\(([A-Z]{2,})\)/);
|
|
60
|
+
if (parenMatch) {
|
|
61
|
+
aliases.push(parenMatch[1]);
|
|
62
|
+
}
|
|
63
|
+
// Add known aliases for any found abbreviation
|
|
64
|
+
for (const alias of [...aliases]) {
|
|
65
|
+
const known = ABBREVIATION_MAP[alias];
|
|
66
|
+
if (known) {
|
|
67
|
+
for (const k of known) {
|
|
68
|
+
if (k.length <= 5 && !aliases.includes(k)) {
|
|
69
|
+
aliases.push(k);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return aliases;
|
|
75
|
+
}
|
|
76
|
+
/** Slugify a heading for anchor links */
|
|
77
|
+
function slugify(text) {
|
|
78
|
+
return text
|
|
79
|
+
.toLowerCase()
|
|
80
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
81
|
+
.replace(/\s+/g, "-")
|
|
82
|
+
.replace(/-+/g, "-")
|
|
83
|
+
.trim();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Parse a single metric markdown file into an array of Metric objects.
|
|
87
|
+
* Splits on ### headings and extracts fields.
|
|
88
|
+
*/
|
|
89
|
+
export function parseMetricsFile(content, filename) {
|
|
90
|
+
const dept = DEPARTMENT_NAMES[filename.replace(".md", "")] || filename.replace(".md", "");
|
|
91
|
+
const metrics = [];
|
|
92
|
+
let currentCategory = "";
|
|
93
|
+
// Split into lines and walk through
|
|
94
|
+
const lines = content.split("\n");
|
|
95
|
+
let i = 0;
|
|
96
|
+
while (i < lines.length) {
|
|
97
|
+
const line = lines[i];
|
|
98
|
+
// Track ## section headings as categories
|
|
99
|
+
if (line.startsWith("## ") && !line.startsWith("### ")) {
|
|
100
|
+
currentCategory = line.replace("## ", "").trim();
|
|
101
|
+
i++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
// Found a ### metric heading
|
|
105
|
+
if (line.startsWith("### ")) {
|
|
106
|
+
const name = line.replace("### ", "").trim();
|
|
107
|
+
const slug = slugify(name);
|
|
108
|
+
const aliases = extractAliases(name);
|
|
109
|
+
// Collect all lines until next ### or ## or end
|
|
110
|
+
const sectionLines = [];
|
|
111
|
+
i++;
|
|
112
|
+
while (i < lines.length &&
|
|
113
|
+
!lines[i].startsWith("### ") &&
|
|
114
|
+
!(lines[i].startsWith("## ") && !lines[i].startsWith("### "))) {
|
|
115
|
+
sectionLines.push(lines[i]);
|
|
116
|
+
i++;
|
|
117
|
+
}
|
|
118
|
+
const rawMarkdown = sectionLines.join("\n").trim();
|
|
119
|
+
const metric = parseSectionFields(name, slug, rawMarkdown, dept, currentCategory, aliases);
|
|
120
|
+
metrics.push(metric);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
i++;
|
|
124
|
+
}
|
|
125
|
+
return metrics;
|
|
126
|
+
}
|
|
127
|
+
/** Extract structured fields from a metric section's markdown body */
|
|
128
|
+
function parseSectionFields(name, slug, raw, department, category, aliases) {
|
|
129
|
+
const definition = extractField(raw, "Definition");
|
|
130
|
+
const insight = extractField(raw, "What it tells you");
|
|
131
|
+
const benchmarks = extractBenchmarks(raw);
|
|
132
|
+
const formulas = extractFormulas(raw);
|
|
133
|
+
const commonMistakes = extractListField(raw, "Common mistakes");
|
|
134
|
+
const sources = extractSources(raw);
|
|
135
|
+
return {
|
|
136
|
+
name,
|
|
137
|
+
slug,
|
|
138
|
+
definition: definition || "",
|
|
139
|
+
formulas,
|
|
140
|
+
benchmarks,
|
|
141
|
+
insight,
|
|
142
|
+
commonMistakes,
|
|
143
|
+
sources,
|
|
144
|
+
department,
|
|
145
|
+
category,
|
|
146
|
+
aliases,
|
|
147
|
+
rawMarkdown: `### ${name}\n\n${raw}`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** Extract a **FieldName:** value from markdown */
|
|
151
|
+
function extractField(text, fieldName) {
|
|
152
|
+
const fence = "\\`\\`\\`";
|
|
153
|
+
const pattern = new RegExp("\\*\\*" + fieldName + ":\\*\\*\\s*(.+?)(?=\\n\\n|\\n\\*\\*|\\n" + fence + "|$)", "s");
|
|
154
|
+
const match = text.match(pattern);
|
|
155
|
+
return match ? match[1].trim() : null;
|
|
156
|
+
}
|
|
157
|
+
/** Extract all code block formulas */
|
|
158
|
+
function extractFormulas(text) {
|
|
159
|
+
const formulas = [];
|
|
160
|
+
const fence = "```";
|
|
161
|
+
const codeBlockRegex = new RegExp(fence + "\\n?([\\s\\S]*?)" + fence, "g");
|
|
162
|
+
let match;
|
|
163
|
+
while ((match = codeBlockRegex.exec(text)) !== null) {
|
|
164
|
+
const formula = match[1].trim();
|
|
165
|
+
if (formula) {
|
|
166
|
+
formulas.push(formula);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return formulas;
|
|
170
|
+
}
|
|
171
|
+
/** Extract benchmarks section — everything after **Benchmarks until next field */
|
|
172
|
+
function extractBenchmarks(text) {
|
|
173
|
+
const pattern = /\*\*Benchmarks[^*]*\*\*\s*([\s\S]*?)(?=\n\*\*[A-Z]|\n##|\n---|\n\*\*Sources|$)/;
|
|
174
|
+
const match = text.match(pattern);
|
|
175
|
+
if (!match)
|
|
176
|
+
return null;
|
|
177
|
+
return match[1].trim() || null;
|
|
178
|
+
}
|
|
179
|
+
/** Extract a bulleted list after **FieldName:** */
|
|
180
|
+
function extractListField(text, fieldName) {
|
|
181
|
+
const pattern = new RegExp(`\\*\\*${fieldName}:\\*\\*\\s*\\n((?:- .+\\n?)+)`, "m");
|
|
182
|
+
const match = text.match(pattern);
|
|
183
|
+
if (!match)
|
|
184
|
+
return null;
|
|
185
|
+
return match[1]
|
|
186
|
+
.split("\n")
|
|
187
|
+
.filter((l) => l.startsWith("- "))
|
|
188
|
+
.map((l) => l.replace(/^- /, "").trim());
|
|
189
|
+
}
|
|
190
|
+
/** Extract source URLs */
|
|
191
|
+
function extractSources(text) {
|
|
192
|
+
const sources = [];
|
|
193
|
+
const pattern = /\*\*Sources:\*\*\s*\n([\s\S]*?)(?=\n\*\*[A-Z]|\n##|\n---|\n$|$)/;
|
|
194
|
+
const match = text.match(pattern);
|
|
195
|
+
if (!match)
|
|
196
|
+
return sources;
|
|
197
|
+
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
198
|
+
let linkMatch;
|
|
199
|
+
while ((linkMatch = linkRegex.exec(match[1])) !== null) {
|
|
200
|
+
sources.push(`${linkMatch[1]}: ${linkMatch[2]}`);
|
|
201
|
+
}
|
|
202
|
+
return sources;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Parse definitions.md into Definition objects.
|
|
206
|
+
* Each ### heading is a term.
|
|
207
|
+
*/
|
|
208
|
+
export function parseDefinitions(content) {
|
|
209
|
+
const definitions = [];
|
|
210
|
+
const lines = content.split("\n");
|
|
211
|
+
let currentSection = "";
|
|
212
|
+
let i = 0;
|
|
213
|
+
while (i < lines.length) {
|
|
214
|
+
const line = lines[i];
|
|
215
|
+
if (line.startsWith("## ") && !line.startsWith("### ")) {
|
|
216
|
+
currentSection = line.replace("## ", "").trim();
|
|
217
|
+
i++;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (line.startsWith("### ")) {
|
|
221
|
+
const term = line.replace("### ", "").trim();
|
|
222
|
+
const slug = slugify(term);
|
|
223
|
+
const sectionLines = [];
|
|
224
|
+
i++;
|
|
225
|
+
while (i < lines.length &&
|
|
226
|
+
!lines[i].startsWith("### ") &&
|
|
227
|
+
!(lines[i].startsWith("## ") && !lines[i].startsWith("### "))) {
|
|
228
|
+
sectionLines.push(lines[i]);
|
|
229
|
+
i++;
|
|
230
|
+
}
|
|
231
|
+
const raw = sectionLines.join("\n").trim();
|
|
232
|
+
// First non-empty line after heading is the definition
|
|
233
|
+
const defLine = raw.split("\n").find((l) => l.trim() && !l.startsWith("---"));
|
|
234
|
+
const definition = defLine?.trim() || "";
|
|
235
|
+
// Extract canonical link
|
|
236
|
+
const linkMatch = raw.match(/\*\*Canonical definition:\*\*\s*\[([^\]]+)\]\(([^)]+)\)/);
|
|
237
|
+
const canonicalLink = linkMatch ? linkMatch[2] : null;
|
|
238
|
+
// Extract "Used in" departments
|
|
239
|
+
const usedInMatch = raw.match(/\*\*Used in:\*\*\s*(.+)/);
|
|
240
|
+
const usedIn = usedInMatch
|
|
241
|
+
? usedInMatch[1].split(",").map((s) => s.trim())
|
|
242
|
+
: [];
|
|
243
|
+
definitions.push({
|
|
244
|
+
term,
|
|
245
|
+
slug,
|
|
246
|
+
definition,
|
|
247
|
+
section: currentSection,
|
|
248
|
+
canonicalLink,
|
|
249
|
+
usedIn,
|
|
250
|
+
});
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
i++;
|
|
254
|
+
}
|
|
255
|
+
return definitions;
|
|
256
|
+
}
|