gasp-standard-mcp 1.9.0 → 1.10.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/README.md +5 -2
- package/build/index.js +69 -42
- package/build/loader.d.ts +16 -3
- package/build/loader.js +50 -12
- package/build/parser.js +11 -0
- package/build/standard-data.json +1 -1
- package/build/standard-version.json +1 -1
- package/build/types.d.ts +10 -0
- package/build/urls.d.ts +20 -0
- package/build/urls.js +31 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,12 +33,13 @@ Restart your MCP client after adding the config.
|
|
|
33
33
|
|
|
34
34
|
| Tool | Description |
|
|
35
35
|
|------|-------------|
|
|
36
|
-
| `lookup_metric` | Full metric details: definition, formula, benchmarks, entity, common mistakes, sources |
|
|
36
|
+
| `lookup_metric` | Full metric details: definition, formula, benchmarks, entity, common mistakes, sources and page URL. Falls back to glossary terms when no metric matches |
|
|
37
|
+
| `lookup_definition` | Glossary term lookup (e.g. Signal Type, Sensitivity Tier, Access Scope, Metric Owner, CEL): full definition, section, canonical or reference link and page URL |
|
|
37
38
|
| `list_metrics` | List all metrics, optionally filtered by department and/or signal type |
|
|
38
39
|
| `get_formula` | Get formula(s) for a metric |
|
|
39
40
|
| `get_relationships` | Upstream/downstream relationships for metrics, entities, or concepts — 21 relationship types across signal, lifecycle, ownership, interaction, and financial flow categories |
|
|
40
41
|
| `get_data_requirements` | Data requirements for implementing a metric: fields, types, grain, source, and example SQL |
|
|
41
|
-
| `search` | Full-text search across metrics, definitions, and data requirements |
|
|
42
|
+
| `search` | Full-text search across metrics, definitions, and data requirements. Metric and definition hits include their gaspwiki.com URL |
|
|
42
43
|
| `generate_query` | Generate warehouse-specific SQL from a metric name + adapter mapping |
|
|
43
44
|
| `validate_adapter` | Validate a GASP adapter mapping file — reports which metrics are fully/partially/not covered |
|
|
44
45
|
| `infer_drivers` | Diagnose what could explain an observed metric movement — ranked upstream candidates with paths and per-step edge classifications |
|
|
@@ -62,6 +63,8 @@ The traversal tools (`infer_drivers`, `explain_path`, `coverage_gap`) return *st
|
|
|
62
63
|
- Access control classifications for every graph metric (sensitivity tier + access scope)
|
|
63
64
|
- CEL `change_nature` field for revenue quality analysis (Adjusted NRR/GRR)
|
|
64
65
|
- Per-department cross-references (e.g. NRR under Customer Success, Gross Margin under Finance) now resolve to the full canonical content via slug lookup
|
|
66
|
+
- Every metric and definition result carries its gaspwiki.com page URL; definitions keep their full body and reference links
|
|
67
|
+
- Access classifications, structured formulas and entity mappings resolve for every graph metric, including cross-reference records (e.g. NRR under Customer Success, CAC under Marketing)
|
|
65
68
|
- Sources: the canonical [GASP Standard](https://gaspwiki.com) files
|
|
66
69
|
|
|
67
70
|
## Examples
|
package/build/index.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { loadStandard, loadBundled, loadRemote, findMetric, getRelationships, search, } from "./loader.js";
|
|
5
|
+
import { loadStandard, loadBundled, loadRemote, findMetric, findDefinition, metricNodeId, getRelationships, search, } from "./loader.js";
|
|
6
6
|
import { buildContext, inferDrivers, findPaths, coverageGap, } from "./traversal.js";
|
|
7
|
+
import { SITE, metricUrl, definitionUrl, resolveDocLink } from "./urls.js";
|
|
7
8
|
// Load data: local repo files → bundled JSON → remote fetch from gaspwiki.com
|
|
8
9
|
let data;
|
|
9
10
|
try {
|
|
@@ -76,15 +77,43 @@ function renderClassifications(path) {
|
|
|
76
77
|
counts.set(s.classification, (counts.get(s.classification) ?? 0) + 1);
|
|
77
78
|
return [...counts.entries()].map(([k, v]) => `${k}×${v}`).join(", ");
|
|
78
79
|
}
|
|
80
|
+
/** Markdown for a glossary definition, with absolute links so agents never guess URLs */
|
|
81
|
+
function formatDefinition(def) {
|
|
82
|
+
const absolutise = (md) => md.replace(/\]\((\/[^)]*)\)/g, `](${SITE}$1)`);
|
|
83
|
+
const parts = [`# ${def.term}`, `**Type:** Definition (${def.section})`, `**URL:** ${definitionUrl(def)}`];
|
|
84
|
+
parts.push(`\n**Definition:** ${absolutise(def.definition)}`);
|
|
85
|
+
if (def.body)
|
|
86
|
+
parts.push(absolutise(def.body));
|
|
87
|
+
const canonical = def.canonicalLink ? resolveDocLink(def.canonicalLink) : null;
|
|
88
|
+
if (canonical)
|
|
89
|
+
parts.push(`\n**Canonical definition:** ${canonical}`);
|
|
90
|
+
const reference = def.referenceLink ? resolveDocLink(def.referenceLink) : null;
|
|
91
|
+
if (reference)
|
|
92
|
+
parts.push(`\n**Reference:** ${reference}`);
|
|
93
|
+
if (def.usedIn.length > 0)
|
|
94
|
+
parts.push(`**Used in:** ${def.usedIn.join(", ")}`);
|
|
95
|
+
return parts.join("\n").replace(/\bEOM Standard\b/g, "GASP Standard");
|
|
96
|
+
}
|
|
79
97
|
// --- Tool 1: lookup_metric ---
|
|
80
|
-
server.tool("lookup_metric", "Look up a GASP Standard metric by name or abbreviation. Returns definition, formula, benchmarks, time period, and
|
|
98
|
+
server.tool("lookup_metric", "Look up a GASP Standard metric by name or abbreviation. Returns definition, formula, benchmarks, time period, department and page URL. Falls back to glossary terms (e.g. 'Signal Type', 'Metric Owner') when no metric matches; use lookup_definition to ask for a term directly.", { name: z.string().describe("Metric name or abbreviation (e.g. 'MRR', 'Net Revenue Retention', 'CAC Payback')") }, async ({ name }) => {
|
|
81
99
|
const metric = findMetric(data, name);
|
|
82
100
|
if (!metric) {
|
|
101
|
+
const def = findDefinition(data, name);
|
|
102
|
+
if (def) {
|
|
103
|
+
return {
|
|
104
|
+
content: [
|
|
105
|
+
{
|
|
106
|
+
type: "text",
|
|
107
|
+
text: `No metric named "${name}". Matching glossary term:\n\n${formatDefinition(def)}`,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
83
112
|
return {
|
|
84
113
|
content: [
|
|
85
114
|
{
|
|
86
115
|
type: "text",
|
|
87
|
-
text: `No metric found matching "${name}". Try
|
|
116
|
+
text: `No metric or definition found matching "${name}". Try the 'search' tool for partial matches.`,
|
|
88
117
|
},
|
|
89
118
|
],
|
|
90
119
|
};
|
|
@@ -99,6 +128,7 @@ server.tool("lookup_metric", "Look up a GASP Standard metric by name or abbrevia
|
|
|
99
128
|
`**Department:** ${metric.department}`,
|
|
100
129
|
`**Category:** ${metric.category}`,
|
|
101
130
|
signalLine,
|
|
131
|
+
`**URL:** ${metricUrl(metric)}`,
|
|
102
132
|
];
|
|
103
133
|
if (metric.definition) {
|
|
104
134
|
parts.push(`\n**Definition:** ${metric.definition}`);
|
|
@@ -125,23 +155,15 @@ server.tool("lookup_metric", "Look up a GASP Standard metric by name or abbrevia
|
|
|
125
155
|
if (metric.aliases.length > 0) {
|
|
126
156
|
parts.push(`\n**Aliases:** ${metric.aliases.join(", ")}`);
|
|
127
157
|
}
|
|
158
|
+
// Graph node id keys every ontology lookup below
|
|
159
|
+
const nodeId = metricNodeId(data, metric);
|
|
128
160
|
// Entity mapping from ontology
|
|
129
|
-
if (data.ontology?.entityMappings) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const matched = metricIds.some(id => lowerAliases.includes(id) ||
|
|
136
|
-
lowerName.startsWith(id.replace(/_/g, '_')) ||
|
|
137
|
-
metric.slug.startsWith(id.replace(/_/g, '-')));
|
|
138
|
-
if (matched) {
|
|
139
|
-
const entity = data.ontology.entities.find(e => e.id === entityId);
|
|
140
|
-
if (entity) {
|
|
141
|
-
parts.push(`\n**Measures:** ${entity.label} (${entity.aliases.join(", ")})`);
|
|
142
|
-
}
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
161
|
+
if (nodeId && data.ontology?.entityMappings) {
|
|
162
|
+
const entityId = Object.entries(data.ontology.entityMappings)
|
|
163
|
+
.find(([, metricIds]) => metricIds.includes(nodeId))?.[0];
|
|
164
|
+
const entity = entityId && data.ontology.entities.find(e => e.id === entityId);
|
|
165
|
+
if (entity) {
|
|
166
|
+
parts.push(`\n**Measures:** ${entity.label} (${entity.aliases.join(", ")})`);
|
|
145
167
|
}
|
|
146
168
|
}
|
|
147
169
|
// Dual-lens content
|
|
@@ -181,27 +203,28 @@ server.tool("lookup_metric", "Look up a GASP Standard metric by name or abbrevia
|
|
|
181
203
|
}
|
|
182
204
|
// Adjusted variant from structured formulas (e.g. NRR, GRR)
|
|
183
205
|
const sfMap = data.ontology?.structuredFormulas;
|
|
184
|
-
if (sfMap) {
|
|
185
|
-
const
|
|
186
|
-
const sf = sfMap[nodeId]
|
|
187
|
-
|| Object.entries(sfMap).find(([k]) => k !== 'description' && nodeId.startsWith(k + '_'))?.[1];
|
|
206
|
+
if (sfMap && nodeId) {
|
|
207
|
+
const sf = sfMap[nodeId];
|
|
188
208
|
if (sf && typeof sf === 'object' && 'adjustedVariant' in sf) {
|
|
189
209
|
parts.push(`\n## Adjusted Variant\n\n${sf.adjustedVariant}`);
|
|
190
210
|
}
|
|
191
211
|
}
|
|
192
212
|
// Access control classification (if available)
|
|
193
213
|
const acMap = data.ontology?.accessControl?.classifications;
|
|
194
|
-
if (acMap) {
|
|
195
|
-
const
|
|
196
|
-
const ac = acMap[nodeId]
|
|
197
|
-
|| Object.entries(acMap).find(([k]) => nodeId.startsWith(k + '_'))?.[1];
|
|
214
|
+
if (acMap && nodeId) {
|
|
215
|
+
const ac = acMap[nodeId];
|
|
198
216
|
if (ac) {
|
|
199
217
|
parts.push(`\n## Recommended Access Control\n` +
|
|
200
|
-
`- **Sensitivity:** ${ac.sensitivityTier} (
|
|
201
|
-
`- **Scope:** ${ac.accessScope} (
|
|
202
|
-
`See
|
|
218
|
+
`- **Sensitivity:** ${ac.sensitivityTier} (${SITE}/v1/definitions/sensitivity-tier)\n` +
|
|
219
|
+
`- **Scope:** ${ac.accessScope} (${SITE}/v1/definitions/access-scope)\n` +
|
|
220
|
+
`See ${SITE}/v1/governance for policy patterns (Cedar, OPA, warehouse RLS).`);
|
|
203
221
|
}
|
|
204
222
|
}
|
|
223
|
+
// A glossary term with the exact name the caller asked for
|
|
224
|
+
const shadowed = data.definitions.find((d) => d.term.toLowerCase() === name.toLowerCase().trim());
|
|
225
|
+
if (shadowed) {
|
|
226
|
+
parts.push(`\n**See also:** glossary term "${shadowed.term}" (${definitionUrl(shadowed)}), via lookup_definition`);
|
|
227
|
+
}
|
|
205
228
|
// Swap EOM → GASP branding
|
|
206
229
|
const text = parts.join("\n").replace(/\bEOM Standard\b/g, "GASP Standard");
|
|
207
230
|
return { content: [{ type: "text", text }] };
|
|
@@ -296,17 +319,8 @@ server.tool("get_formula", "Get the formula(s) for a GASP Standard metric. Retur
|
|
|
296
319
|
}
|
|
297
320
|
// Append structured formula references if available
|
|
298
321
|
const formulas = data.ontology?.structuredFormulas;
|
|
299
|
-
const nodeId =
|
|
300
|
-
|
|
301
|
-
let structured = formulas?.[nodeId];
|
|
302
|
-
if (!structured && formulas) {
|
|
303
|
-
for (const [key, val] of Object.entries(formulas)) {
|
|
304
|
-
if (key !== 'description' && nodeId.startsWith(key + '_')) {
|
|
305
|
-
structured = val;
|
|
306
|
-
break;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
322
|
+
const nodeId = metricNodeId(data, metric);
|
|
323
|
+
const structured = nodeId ? formulas?.[nodeId] : undefined;
|
|
310
324
|
if (structured) {
|
|
311
325
|
if (structured.inputs && structured.inputs.length > 0) {
|
|
312
326
|
parts.push("## Inputs (metric dependencies)");
|
|
@@ -453,7 +467,8 @@ server.tool("search", "Search across all GASP Standard metrics and definitions.
|
|
|
453
467
|
for (const r of results) {
|
|
454
468
|
const dept = r.department ? ` [${r.department}]` : "";
|
|
455
469
|
parts.push(`### ${r.name}${dept}`);
|
|
456
|
-
parts.push(`*${r.type}*: ${r.snippet}
|
|
470
|
+
parts.push(`*${r.type}*: ${r.snippet}`);
|
|
471
|
+
parts.push(r.url ? `URL: ${r.url}\n` : "");
|
|
457
472
|
}
|
|
458
473
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
459
474
|
});
|
|
@@ -824,6 +839,18 @@ server.tool("coverage_gap", "Audit a metric set against the GASP relationship gr
|
|
|
824
839
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
825
840
|
});
|
|
826
841
|
// Start the server
|
|
842
|
+
// --- Tool 12: lookup_definition ---
|
|
843
|
+
server.tool("lookup_definition", "Look up a GASP Standard glossary term (e.g. 'Signal Type', 'Sensitivity Tier', 'Access Scope', 'Metric Owner', 'Churn', 'CEL'). Returns the full definition, its section, related links and page URL. Use lookup_metric for metrics.", { term: z.string().describe("Term name, abbreviation or slug (e.g. 'Signal Type', 'CEL', 'metric-owner')") }, async ({ term }) => {
|
|
844
|
+
const def = findDefinition(data, term);
|
|
845
|
+
if (!def) {
|
|
846
|
+
const metric = findMetric(data, term);
|
|
847
|
+
const hint = metric
|
|
848
|
+
? ` "${metric.name}" is a metric: use lookup_metric (${metricUrl(metric)}).`
|
|
849
|
+
: " Try the 'search' tool for partial matches.";
|
|
850
|
+
return { content: [{ type: "text", text: `No definition found matching "${term}".${hint}` }] };
|
|
851
|
+
}
|
|
852
|
+
return { content: [{ type: "text", text: formatDefinition(def) }] };
|
|
853
|
+
});
|
|
827
854
|
async function main() {
|
|
828
855
|
const transport = new StdioServerTransport();
|
|
829
856
|
await server.connect(transport);
|
package/build/loader.d.ts
CHANGED
|
@@ -1,22 +1,35 @@
|
|
|
1
|
-
import { Metric, RelationshipNode, StandardData } from "./types.js";
|
|
1
|
+
import { Metric, Definition, RelationshipNode, StandardData } from "./types.js";
|
|
2
2
|
/** Load pre-parsed data bundled with the npm package (offline fallback) */
|
|
3
3
|
export declare function loadBundled(): StandardData;
|
|
4
4
|
/** Fetch pre-parsed standard data from gaspwiki.com (for global/npx installs) */
|
|
5
5
|
export declare function loadRemote(): Promise<StandardData>;
|
|
6
6
|
/** Load all standard data from local files at startup */
|
|
7
7
|
export declare function loadStandard(): StandardData;
|
|
8
|
+
/**
|
|
9
|
+
* Graph node id for a metric record, used to key ontology lookups.
|
|
10
|
+
* Data bundled before metricNodeIds existed falls back to an exact slug match.
|
|
11
|
+
*/
|
|
12
|
+
export declare function metricNodeId(data: StandardData, metric: Pick<Metric, "slug">): string | undefined;
|
|
8
13
|
/**
|
|
9
14
|
* Search metrics + definitions for a query string.
|
|
10
15
|
* Returns top N matches ranked by relevance.
|
|
11
16
|
*/
|
|
12
|
-
export
|
|
17
|
+
export interface SearchResult {
|
|
13
18
|
type: "metric" | "definition" | "data_requirement";
|
|
14
19
|
name: string;
|
|
15
20
|
snippet: string;
|
|
16
21
|
department?: string;
|
|
17
|
-
|
|
22
|
+
/** gaspwiki.com page for this result (absent for data requirements) */
|
|
23
|
+
url?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function search(data: StandardData, query: string, limit?: number): SearchResult[];
|
|
18
26
|
/** Find a metric by name, abbreviation, or slug */
|
|
19
27
|
export declare function findMetric(data: StandardData, name: string): Metric | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Find a glossary definition by term, slug or abbreviation.
|
|
30
|
+
* "CEL" matches "CEL (Commercial Event Ledger)" via the parenthetical.
|
|
31
|
+
*/
|
|
32
|
+
export declare function findDefinition(data: StandardData, name: string): Definition | undefined;
|
|
20
33
|
/** Get relationships for a metric: upstream (what drives it) and downstream (what it drives) */
|
|
21
34
|
export declare function getRelationships(data: StandardData, metricId: string): {
|
|
22
35
|
node: RelationshipNode | undefined;
|
package/build/loader.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync, readdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { parseMetricsFile, parseDefinitions } from "./parser.js";
|
|
4
|
+
import { metricUrl, definitionUrl } from "./urls.js";
|
|
4
5
|
/** Resolve the repo root (two levels up from build/index.js) */
|
|
5
6
|
function repoRoot() {
|
|
6
7
|
// gasp-wiki-mcp/build/index.js -> gasp-wiki-mcp -> repo root
|
|
@@ -42,6 +43,24 @@ export function loadStandard() {
|
|
|
42
43
|
const relData = JSON.parse(readFileSync(relPath, "utf-8"));
|
|
43
44
|
const nodes = relData.nodes;
|
|
44
45
|
const edges = relData.edges;
|
|
46
|
+
// Metric slug → graph node id, from the website's node → page map.
|
|
47
|
+
// Stubs that share the canonical slug resolve directly.
|
|
48
|
+
const linksPath = join(root, "gasp-wiki-frontend", "src", "data", "metric-links.ts");
|
|
49
|
+
const metricNodeIds = {};
|
|
50
|
+
const linkRe = /^\s*(\w+):\s*\{\s*href:\s*'\/v1\/metrics\/[a-z0-9-]+\/([a-z0-9-]+)'/gm;
|
|
51
|
+
for (const [, nodeId, slug] of readFileSync(linksPath, "utf-8").matchAll(linkRe)) {
|
|
52
|
+
metricNodeIds[slug] = nodeId;
|
|
53
|
+
}
|
|
54
|
+
// Node id → canonical slug, taken before stubs are added
|
|
55
|
+
const slugByNodeId = new Map(Object.entries(metricNodeIds).map(([slug, id]) => [id, slug]));
|
|
56
|
+
// Stubs with their own slug ("See [Core](core.md#cac-customer-acquisition-cost)")
|
|
57
|
+
// inherit the node of the metric they point to.
|
|
58
|
+
for (const m of metrics) {
|
|
59
|
+
const anchor = m.rawMarkdown.match(/See \[[^\]]+\]\([^)#]*#([^)]+)\)/)?.[1];
|
|
60
|
+
if (anchor && metricNodeIds[anchor] && !metricNodeIds[m.slug]) {
|
|
61
|
+
metricNodeIds[m.slug] = metricNodeIds[anchor];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
45
64
|
// Load data requirements from dashboard
|
|
46
65
|
const dashPath = join(root, "gasp-wiki-frontend", "src", "data", "dashboard-mock.json");
|
|
47
66
|
const dashData = JSON.parse(readFileSync(dashPath, "utf-8"));
|
|
@@ -206,24 +225,24 @@ export function loadStandard() {
|
|
|
206
225
|
node.signalType = node.signalType || '';
|
|
207
226
|
node.category = node.category || node.department || '';
|
|
208
227
|
if (!node.description) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
// Search for a matching definition
|
|
212
|
-
for (const [key, def] of defBySlug) {
|
|
213
|
-
if (key.includes(slug) || slug.includes(key.split('-')[0])) {
|
|
214
|
-
node.description = def;
|
|
215
|
-
break;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
228
|
+
const slug = slugByNodeId.get(node.id);
|
|
229
|
+
node.description = (slug && defBySlug.get(slug)) || '';
|
|
218
230
|
}
|
|
219
231
|
}
|
|
220
232
|
}
|
|
221
|
-
return { metrics, definitions, nodes, edges, dataRequirements, ontology, queryTemplates };
|
|
233
|
+
return { metrics, definitions, nodes, edges, dataRequirements, ontology, queryTemplates, metricNodeIds };
|
|
222
234
|
}
|
|
223
235
|
/**
|
|
224
|
-
*
|
|
225
|
-
*
|
|
236
|
+
* Graph node id for a metric record, used to key ontology lookups.
|
|
237
|
+
* Data bundled before metricNodeIds existed falls back to an exact slug match.
|
|
226
238
|
*/
|
|
239
|
+
export function metricNodeId(data, metric) {
|
|
240
|
+
const mapped = data.metricNodeIds?.[metric.slug];
|
|
241
|
+
if (mapped)
|
|
242
|
+
return mapped;
|
|
243
|
+
const id = metric.slug.replace(/-/g, "_");
|
|
244
|
+
return data.nodes.some((n) => n.id === id) ? id : undefined;
|
|
245
|
+
}
|
|
227
246
|
export function search(data, query, limit = 10) {
|
|
228
247
|
const q = query.toLowerCase();
|
|
229
248
|
const results = [];
|
|
@@ -236,6 +255,7 @@ export function search(data, query, limit = 10) {
|
|
|
236
255
|
name: m.name,
|
|
237
256
|
snippet: `[${signalLabel}] ${m.definition || m.insight || ""}`,
|
|
238
257
|
department: m.department,
|
|
258
|
+
url: metricUrl(m),
|
|
239
259
|
score,
|
|
240
260
|
});
|
|
241
261
|
}
|
|
@@ -247,6 +267,7 @@ export function search(data, query, limit = 10) {
|
|
|
247
267
|
type: "definition",
|
|
248
268
|
name: d.term,
|
|
249
269
|
snippet: d.definition,
|
|
270
|
+
url: definitionUrl(d),
|
|
250
271
|
score,
|
|
251
272
|
});
|
|
252
273
|
}
|
|
@@ -332,6 +353,23 @@ export function findMetric(data, name) {
|
|
|
332
353
|
data.metrics.find((m) => m.name.toLowerCase().includes(q)) ||
|
|
333
354
|
data.metrics.find((m) => m.aliases.some((a) => a.toLowerCase().includes(q))));
|
|
334
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Find a glossary definition by term, slug or abbreviation.
|
|
358
|
+
* "CEL" matches "CEL (Commercial Event Ledger)" via the parenthetical.
|
|
359
|
+
*/
|
|
360
|
+
export function findDefinition(data, name) {
|
|
361
|
+
const q = name.toLowerCase().trim();
|
|
362
|
+
if (!q)
|
|
363
|
+
return undefined;
|
|
364
|
+
const parts = (term) => {
|
|
365
|
+
const m = term.match(/^(.+?)\s*\(([^)]+)\)\s*$/);
|
|
366
|
+
return m ? [m[1].toLowerCase().trim(), m[2].toLowerCase().trim()] : [];
|
|
367
|
+
};
|
|
368
|
+
return (data.definitions.find((d) => d.term.toLowerCase() === q) ||
|
|
369
|
+
data.definitions.find((d) => d.slug === q) ||
|
|
370
|
+
data.definitions.find((d) => parts(d.term).includes(q)) ||
|
|
371
|
+
data.definitions.find((d) => d.term.toLowerCase().includes(q)));
|
|
372
|
+
}
|
|
335
373
|
/** Get relationships for a metric: upstream (what drives it) and downstream (what it drives) */
|
|
336
374
|
export function getRelationships(data, metricId) {
|
|
337
375
|
const q = metricId.toLowerCase().trim();
|
package/build/parser.js
CHANGED
|
@@ -346,6 +346,15 @@ export function parseDefinitions(content) {
|
|
|
346
346
|
// Extract canonical link
|
|
347
347
|
const linkMatch = raw.match(/\*\*Canonical definition:\*\*\s*\[([^\]]+)\]\(([^)]+)\)/);
|
|
348
348
|
const canonicalLink = linkMatch ? linkMatch[2] : null;
|
|
349
|
+
// Extract reference link (framework terms point at a site page instead)
|
|
350
|
+
const refMatch = raw.match(/\*\*Reference:\*\*\s*\[([^\]]+)\]\(([^)]+)\)/);
|
|
351
|
+
const referenceLink = refMatch ? refMatch[2] : null;
|
|
352
|
+
// Remaining body (bullets, examples) minus the link/metadata lines
|
|
353
|
+
const bodyLines = raw.split("\n").filter((l) => l !== defLine && !l.startsWith("---"));
|
|
354
|
+
const body = bodyLines
|
|
355
|
+
.filter((l) => !/\*\*(Canonical definition|Reference|Used in):\*\*/.test(l))
|
|
356
|
+
.join("\n")
|
|
357
|
+
.trim();
|
|
349
358
|
// Extract "Used in" departments
|
|
350
359
|
const usedInMatch = raw.match(/\*\*Used in:\*\*\s*(.+)/);
|
|
351
360
|
const usedIn = usedInMatch
|
|
@@ -357,6 +366,8 @@ export function parseDefinitions(content) {
|
|
|
357
366
|
definition,
|
|
358
367
|
section: currentSection,
|
|
359
368
|
canonicalLink,
|
|
369
|
+
referenceLink,
|
|
370
|
+
body: body || null,
|
|
360
371
|
usedIn,
|
|
361
372
|
});
|
|
362
373
|
continue;
|