gasp-standard-mcp 1.7.1 → 1.8.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 CHANGED
@@ -56,6 +56,7 @@ Restart your MCP client after adding the config.
56
56
  - 60 temporal mappings (grain, frequency, period for every metric node)
57
57
  - Access control classifications for every graph metric (sensitivity tier + access scope)
58
58
  - CEL `change_nature` field for revenue quality analysis (Adjusted NRR/GRR)
59
+ - Per-department cross-references (e.g. NRR under Customer Success, Gross Margin under Finance) now resolve to the full canonical content via slug lookup
59
60
  - Sources: the canonical [GASP Standard](https://gaspwiki.com) files
60
61
 
61
62
  ## Examples
package/build/loader.js CHANGED
@@ -97,6 +97,57 @@ export function loadStandard() {
97
97
  }
98
98
  }
99
99
  }
100
+ // Enrich cross-reference stubs with canonical content fields.
101
+ //
102
+ // Some metrics intentionally appear in multiple department files — e.g. NRR
103
+ // is defined canonically in core.md and listed as a "See [Core](core.md#...)"
104
+ // stub in customer-success.md. Per-department records are needed to drive
105
+ // department-scoped routes (e.g. /v1/metrics/customer-success/...), but
106
+ // consumers of standard.json that look up by slug expect every record to
107
+ // carry full content. Without this pass, Map-based lookups silently get the
108
+ // empty stub instead of the canonical record.
109
+ //
110
+ // Strategy: keep stub identity (department, category, name, slug, rawMarkdown)
111
+ // but copy any content field that's empty on the stub from the canonical.
112
+ const canonicalBySlug = new Map();
113
+ for (const m of metrics) {
114
+ if (m.slug && m.definition && !canonicalBySlug.has(m.slug)) {
115
+ canonicalBySlug.set(m.slug, m);
116
+ }
117
+ }
118
+ for (const m of metrics) {
119
+ if (m.definition)
120
+ continue; // already canonical or non-stub
121
+ const seeMatch = m.rawMarkdown.match(/See \[([^\]]+)\]\(([^)]+)\)/);
122
+ if (!seeMatch)
123
+ continue;
124
+ const link = seeMatch[2];
125
+ const anchor = link.includes("#") ? link.split("#")[1] : null;
126
+ if (!anchor)
127
+ continue;
128
+ const canon = canonicalBySlug.get(anchor) ||
129
+ canonicalBySlug.get(m.slug) ||
130
+ [...canonicalBySlug.values()].find((c) => c.slug.includes(anchor));
131
+ if (!canon || canon === m)
132
+ continue;
133
+ if (!m.definition)
134
+ m.definition = canon.definition;
135
+ if (!m.formulas?.length)
136
+ m.formulas = [...canon.formulas];
137
+ if (!m.benchmarks)
138
+ m.benchmarks = canon.benchmarks;
139
+ if (!m.insight)
140
+ m.insight = canon.insight;
141
+ if (!m.commonMistakes?.length && canon.commonMistakes)
142
+ m.commonMistakes = [...canon.commonMistakes];
143
+ if (!m.sources?.length)
144
+ m.sources = [...canon.sources];
145
+ if (!m.aliases?.length)
146
+ m.aliases = [...canon.aliases];
147
+ // dualLens and dualLensNote are intentionally NOT enriched — they are a
148
+ // Core-only construct (asserted by data-integrity tests). Consumers that
149
+ // need dual-lens content should resolve via slug to the Core record.
150
+ }
100
151
  // Load query templates
101
152
  const queryTemplatePath = join(standardDir, "query-templates.json");
102
153
  let queryTemplates;