fullstackgtm 0.19.0 → 0.20.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/CHANGELOG.md +38 -0
- package/dist/cli.js +89 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/market.d.ts +28 -0
- package/dist/marketOverlay.d.ts +116 -0
- package/dist/marketOverlay.js +258 -0
- package/dist/marketReport.js +16 -3
- package/dist/marketScale.d.ts +42 -0
- package/dist/marketScale.js +68 -0
- package/package.json +1 -1
- package/src/cli.ts +108 -1
- package/src/index.ts +16 -0
- package/src/market.ts +29 -0
- package/src/marketOverlay.ts +410 -0
- package/src/marketReport.ts +20 -4
- package/src/marketScale.ts +111 -0
package/dist/marketReport.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { computeFrontStates } from "./market.js";
|
|
2
2
|
import { assessAxes, messageBreadth } from "./marketAxes.js";
|
|
3
|
+
import { computeScaleIndex } from "./marketScale.js";
|
|
3
4
|
/**
|
|
4
5
|
* Render a market map as a client-ready deliverable: markdown for terminals
|
|
5
6
|
* and PRs, and a self-contained printable HTML "field report" — front
|
|
@@ -98,7 +99,8 @@ function svgScatter(points, ax, ay, anchor, mini) {
|
|
|
98
99
|
const e = escapeHtml;
|
|
99
100
|
const dots = points
|
|
100
101
|
.map((p) => {
|
|
101
|
-
|
|
102
|
+
// Area-proportional: perceived bubble area tracks the size metric.
|
|
103
|
+
const r = (mini ? 4 + 14 * Math.sqrt(p.size) : 8 + 26 * Math.sqrt(p.size));
|
|
102
104
|
const cls = p.vendorId === anchor ? "dot-anchor" : "dot";
|
|
103
105
|
return (`<circle class="${cls}" cx="${sx(p.x).toFixed(1)}" cy="${sy(p.y).toFixed(1)}" r="${r.toFixed(1)}"/>` +
|
|
104
106
|
`<text class="dot-label" style="font-size:${fsLabel}px" x="${sx(p.x).toFixed(1)}" y="${(sy(p.y) - r - 4).toFixed(1)}">${e(p.name)}</text>`);
|
|
@@ -121,7 +123,18 @@ function axisSectionsHtml(config, set) {
|
|
|
121
123
|
const e = escapeHtml;
|
|
122
124
|
const report = assessAxes(config, set);
|
|
123
125
|
const vendorNames = new Map(config.vendors.map((vendor) => [vendor.id, vendor.name]));
|
|
126
|
+
// Bubble size: scale index (relative market scale from citable signals)
|
|
127
|
+
// when every placeable vendor has one; LOUD count otherwise — never mix
|
|
128
|
+
// the two semantics on one chart.
|
|
129
|
+
const scale = computeScaleIndex(config);
|
|
130
|
+
const scaleIndex = new Map(scale.vendors.map((vendor) => [vendor.vendorId, vendor.index]));
|
|
131
|
+
const useScale = report.vendors.length > 0 && report.vendors.every((vendorId) => scaleIndex.get(vendorId) !== null && scaleIndex.get(vendorId) !== undefined);
|
|
124
132
|
const loudCounts = new Map(report.vendors.map((vendorId) => [vendorId, messageBreadth(vendorId, set.observations).loudCount]));
|
|
133
|
+
const maxLoud = Math.max(1, ...loudCounts.values());
|
|
134
|
+
const sizeOf = (vendorId) => useScale ? scaleIndex.get(vendorId) : (loudCounts.get(vendorId) ?? 0) / maxLoud;
|
|
135
|
+
const sizeCaption = useScale
|
|
136
|
+
? `Dot area ∝ relative scale index (within this vendor set, from: ${e(scale.metricsUsed.join(", "))} — citable signals, not true market share)`
|
|
137
|
+
: "Dot area ∝ LOUD count";
|
|
125
138
|
const breadthAxis = {
|
|
126
139
|
id: "breadth",
|
|
127
140
|
label: "Message breadth",
|
|
@@ -158,7 +171,7 @@ function axisSectionsHtml(config, set) {
|
|
|
158
171
|
name: vendorNames.get(vendorId) ?? vendorId,
|
|
159
172
|
x: xs.get(vendorId),
|
|
160
173
|
y: ys.get(vendorId),
|
|
161
|
-
|
|
174
|
+
size: sizeOf(vendorId),
|
|
162
175
|
}));
|
|
163
176
|
};
|
|
164
177
|
const [px, py] = config.primaryAxes ?? [axes[0].id, axes[1]?.id ?? "breadth"];
|
|
@@ -170,7 +183,7 @@ function axisSectionsHtml(config, set) {
|
|
|
170
183
|
<figure>${svgScatter(pointsFor(px, py), axInfo, ayInfo, config.anchorVendor, false)}
|
|
171
184
|
<figcaption>Positions computed from run ${e(set.runLabel)} observations: each axis is a per-claim scoring rubric
|
|
172
185
|
in the market config; a vendor sits at the intensity-weighted mean (loud=1, quiet=½) of the claims it
|
|
173
|
-
voices.
|
|
186
|
+
voices. ${sizeCaption}. Axis status — ${e(axInfo.label)}: ${e(statusOf(px))}; ${e(ayInfo.label)}: ${e(statusOf(py))}.</figcaption>
|
|
174
187
|
</figure>
|
|
175
188
|
</section>`;
|
|
176
189
|
// Deliberately no axis-pairing gallery here: the report is the client-facing
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { MarketConfig, ScaleSignal } from "./market.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Relative scale index over the mapped vendor set — the honest version of
|
|
4
|
+
* "bubble size = market share". True segment market share is unknowable from
|
|
5
|
+
* public data for mostly-private vendor sets, so this computes a composite
|
|
6
|
+
* index from whatever citable signals exist per vendor (review counts,
|
|
7
|
+
* headcount, disclosed revenue, self-reported customers), each of which is
|
|
8
|
+
* biased in a different direction; the composite triangulates.
|
|
9
|
+
*
|
|
10
|
+
* Method, deterministic and auditable:
|
|
11
|
+
* 1. Per metric, log10(value + 1) — these signals span orders of magnitude.
|
|
12
|
+
* 2. Normalize each metric to [0, 1] across the vendors that HAVE it
|
|
13
|
+
* (min–max within the set; a metric only one vendor has is skipped —
|
|
14
|
+
* it cannot rank anyone).
|
|
15
|
+
* 3. A vendor's index = arithmetic mean of its normalized metric scores
|
|
16
|
+
* (mean-of-normalized rather than geometric-of-raw so missing signals
|
|
17
|
+
* neither punish nor reward), reported with coverage (which metrics).
|
|
18
|
+
*
|
|
19
|
+
* Vendors with zero signals get index null — the report falls back to its
|
|
20
|
+
* LOUD-count sizing for the whole map rather than mixing semantics.
|
|
21
|
+
*/
|
|
22
|
+
export type VendorScale = {
|
|
23
|
+
vendorId: string;
|
|
24
|
+
/** [0, 1] within the mapped set; null when the vendor has no usable signals. */
|
|
25
|
+
index: number | null;
|
|
26
|
+
/** Metrics that contributed, with their normalized scores. */
|
|
27
|
+
coverage: Array<{
|
|
28
|
+
metric: string;
|
|
29
|
+
value: number;
|
|
30
|
+
normalized: number;
|
|
31
|
+
}>;
|
|
32
|
+
signals: ScaleSignal[];
|
|
33
|
+
};
|
|
34
|
+
export type ScaleReport = {
|
|
35
|
+
vendors: VendorScale[];
|
|
36
|
+
/** Metrics used (present for ≥2 vendors) and metrics skipped (singletons). */
|
|
37
|
+
metricsUsed: string[];
|
|
38
|
+
metricsSkipped: string[];
|
|
39
|
+
complete: boolean;
|
|
40
|
+
};
|
|
41
|
+
export declare function computeScaleIndex(config: MarketConfig): ScaleReport;
|
|
42
|
+
export declare function scaleReportToText(config: MarketConfig, report: ScaleReport): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export function computeScaleIndex(config) {
|
|
2
|
+
const byMetric = new Map();
|
|
3
|
+
for (const vendor of config.vendors) {
|
|
4
|
+
for (const signal of vendor.scaleSignals ?? []) {
|
|
5
|
+
if (!Number.isFinite(signal.value) || signal.value < 0)
|
|
6
|
+
continue;
|
|
7
|
+
const rows = byMetric.get(signal.metric) ?? [];
|
|
8
|
+
rows.push({ vendorId: vendor.id, value: signal.value });
|
|
9
|
+
byMetric.set(signal.metric, rows);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const metricsUsed = [];
|
|
13
|
+
const metricsSkipped = [];
|
|
14
|
+
const normalized = new Map();
|
|
15
|
+
for (const [metric, rows] of byMetric) {
|
|
16
|
+
// Last write wins if a vendor lists the same metric twice.
|
|
17
|
+
const perVendor = new Map(rows.map((row) => [row.vendorId, row.value]));
|
|
18
|
+
if (perVendor.size < 2) {
|
|
19
|
+
metricsSkipped.push(metric);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
metricsUsed.push(metric);
|
|
23
|
+
const logs = new Map([...perVendor].map(([vendorId, value]) => [vendorId, Math.log10(value + 1)]));
|
|
24
|
+
const values = [...logs.values()];
|
|
25
|
+
const lo = Math.min(...values);
|
|
26
|
+
const hi = Math.max(...values);
|
|
27
|
+
const span = hi - lo || 1;
|
|
28
|
+
const scores = new Map();
|
|
29
|
+
for (const [vendorId, log] of logs) {
|
|
30
|
+
scores.set(vendorId, { value: perVendor.get(vendorId), normalized: (log - lo) / span });
|
|
31
|
+
}
|
|
32
|
+
normalized.set(metric, scores);
|
|
33
|
+
}
|
|
34
|
+
metricsUsed.sort();
|
|
35
|
+
metricsSkipped.sort();
|
|
36
|
+
const vendors = config.vendors.map((vendor) => {
|
|
37
|
+
const coverage = [];
|
|
38
|
+
for (const metric of metricsUsed) {
|
|
39
|
+
const score = normalized.get(metric)?.get(vendor.id);
|
|
40
|
+
if (score)
|
|
41
|
+
coverage.push({ metric, value: score.value, normalized: Number(score.normalized.toFixed(4)) });
|
|
42
|
+
}
|
|
43
|
+
const index = coverage.length > 0
|
|
44
|
+
? Number((coverage.reduce((sum, entry) => sum + entry.normalized, 0) / coverage.length).toFixed(4))
|
|
45
|
+
: null;
|
|
46
|
+
return { vendorId: vendor.id, index, coverage, signals: vendor.scaleSignals ?? [] };
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
vendors,
|
|
50
|
+
metricsUsed,
|
|
51
|
+
metricsSkipped,
|
|
52
|
+
complete: vendors.every((vendor) => vendor.index !== null),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function scaleReportToText(config, report) {
|
|
56
|
+
const names = new Map(config.vendors.map((vendor) => [vendor.id, vendor.name]));
|
|
57
|
+
const lines = [];
|
|
58
|
+
lines.push(`Scale index (relative, within this ${config.vendors.length}-vendor set — not market share):`);
|
|
59
|
+
lines.push(`metrics used: ${report.metricsUsed.join(", ") || "none"}${report.metricsSkipped.length ? ` · skipped (single-vendor): ${report.metricsSkipped.join(", ")}` : ""}`);
|
|
60
|
+
lines.push("");
|
|
61
|
+
const ranked = [...report.vendors].sort((a, b) => (b.index ?? -1) - (a.index ?? -1));
|
|
62
|
+
for (const vendor of ranked) {
|
|
63
|
+
const idx = vendor.index === null ? " n/a" : vendor.index.toFixed(2);
|
|
64
|
+
const cov = vendor.coverage.map((entry) => `${entry.metric}=${entry.value}`).join(", ") || "no signals";
|
|
65
|
+
lines.push(` ${idx} ${(names.get(vendor.vendorId) ?? vendor.vendorId).padEnd(22)} ${cov}`);
|
|
66
|
+
}
|
|
67
|
+
return `${lines.join("\n")}\n`;
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fullstackgtm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Open-source agentic GTM ops framework: canonical GTM data model, pluggable deterministic audits, reviewable dry-run patch plans, approval-gated write-back with conflict detection, and cross-system entity resolution. HubSpot, Salesforce, and Stripe connectors included.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Full Stack GTM",
|
package/src/cli.ts
CHANGED
|
@@ -52,6 +52,14 @@ import {
|
|
|
52
52
|
type ObservationSet,
|
|
53
53
|
} from "./market.ts";
|
|
54
54
|
import { assessAxes, axesReportToText } from "./marketAxes.ts";
|
|
55
|
+
import {
|
|
56
|
+
computeDirectives,
|
|
57
|
+
computeOverlayStats,
|
|
58
|
+
directivesToPlan,
|
|
59
|
+
overlayToMarkdown,
|
|
60
|
+
type CallDocument,
|
|
61
|
+
} from "./marketOverlay.ts";
|
|
62
|
+
import { computeScaleIndex, scaleReportToText } from "./marketScale.ts";
|
|
55
63
|
import { buildWorksheet, classifyMarket } from "./marketClassify.ts";
|
|
56
64
|
import { marketMapToHtml, marketMapToMarkdown } from "./marketReport.ts";
|
|
57
65
|
import {
|
|
@@ -118,6 +126,8 @@ Usage:
|
|
|
118
126
|
fullstackgtm market fronts [--run <label>] [--diff <prior-run>] [--json]
|
|
119
127
|
fullstackgtm market axes [--run <label>] [--json]
|
|
120
128
|
fullstackgtm market report [--run <label>] [--format md|html] [--out <path>]
|
|
129
|
+
fullstackgtm market overlay --snapshot <crm.json> [--calls <files>] [--save]
|
|
130
|
+
fullstackgtm market scale [--json]
|
|
121
131
|
fullstackgtm market refresh [--run <label>] [--model m]
|
|
122
132
|
the live competitive map: capture vendor pages (content-addressed),
|
|
123
133
|
classify intensity per claim (LLM bring-your-own-key, or fill the
|
|
@@ -875,9 +885,24 @@ market worksheet --vendor <id> [--capture-run <label>] [--out <path>]
|
|
|
875
885
|
market observe --from <observations.json> [--unverified]
|
|
876
886
|
market fronts [--config <path>] [--run <label>] [--diff <prior-run>] [--json]
|
|
877
887
|
market axes [--config <path>] [--run <label>] [--json]
|
|
888
|
+
market overlay --snapshot <crm.json> [--calls <parsed.json|manifest.json>]... [--prior-run <label>]
|
|
889
|
+
[--min-mentions N] [--promote-lift X] [--json] [--save --task-account <id>|--task-deal <id>]
|
|
890
|
+
market scale [--config <path>] [--json]
|
|
878
891
|
market report [--config <path>] [--run <label>] [--format md|html] [--out <path>]
|
|
879
892
|
market refresh [--run <label>] [--model m] capture → classify → fronts drift → HTML report
|
|
880
893
|
|
|
894
|
+
overlay is the directive layer: joins the map to YOUR CRM ground truth and
|
|
895
|
+
emits OCCUPY / PROMOTE / URGENT / RETREAT directives, each carrying ≥1
|
|
896
|
+
observation and ≥1 CRM statistic with its sample size. Claim mentions are
|
|
897
|
+
deterministic word-boundary matches of each claim's "terms" against call
|
|
898
|
+
documents (call parse output); small samples refuse to become strategy
|
|
899
|
+
(--min-mentions, default 3). --save turns directives into approval-gated
|
|
900
|
+
create_task operations through the normal plans → approve → apply gate.
|
|
901
|
+
|
|
902
|
+
scale prints the relative scale index that sizes the report's bubbles when
|
|
903
|
+
vendors carry scaleSignals (citable review counts / headcount / revenue —
|
|
904
|
+
a within-set index, never "market share" unqualified).
|
|
905
|
+
|
|
881
906
|
axes runs the axis-discovery math: PCA over the vendor × claim intensity
|
|
882
907
|
matrix (PC1 = the category's primary axis, PC2 = the max-differentiation
|
|
883
908
|
direction orthogonal to it), triangulation of configured axes against the
|
|
@@ -1089,8 +1114,90 @@ recomputed deterministically on every invocation — never stored.`);
|
|
|
1089
1114
|
return;
|
|
1090
1115
|
}
|
|
1091
1116
|
|
|
1117
|
+
if (subcommand === "scale") {
|
|
1118
|
+
const report = computeScaleIndex(config);
|
|
1119
|
+
if (rest.includes("--json")) {
|
|
1120
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
console.log(scaleReportToText(config, report));
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
if (subcommand === "overlay") {
|
|
1128
|
+
const set = await loadSet();
|
|
1129
|
+
const snapshotPath = option(rest, "--snapshot");
|
|
1130
|
+
if (!snapshotPath) {
|
|
1131
|
+
throw new Error(
|
|
1132
|
+
"market overlay requires --snapshot <canonical-snapshot.json> (fullstackgtm snapshot --out it first) — directives need CRM ground truth",
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
const snapshot = JSON.parse(readFileSync(resolve(process.cwd(), snapshotPath), "utf8")) as CanonicalGtmSnapshot;
|
|
1136
|
+
|
|
1137
|
+
// --calls accepts ParsedCall JSON files (from `call parse --out`) and/or
|
|
1138
|
+
// manifest arrays [{path, dealId?}] linking calls to deals. Repeatable.
|
|
1139
|
+
const documents: CallDocument[] = [];
|
|
1140
|
+
const addParsedCall = (parsedPath: string, dealId?: string) => {
|
|
1141
|
+
const parsed = JSON.parse(readFileSync(resolve(process.cwd(), parsedPath), "utf8")) as ParsedCall & {
|
|
1142
|
+
segments?: Array<{ text?: string }>;
|
|
1143
|
+
};
|
|
1144
|
+
const text = [
|
|
1145
|
+
...(parsed.segments ?? []).map((segment) => segment.text ?? ""),
|
|
1146
|
+
...(parsed.insights ?? []).map((insight) => `${insight.text ?? ""} ${insight.evidence ?? ""}`),
|
|
1147
|
+
].join("\n");
|
|
1148
|
+
documents.push({ id: parsed.id ?? parsedPath, text, dealId, occurredAt: parsed.evidence?.[0]?.capturedAt });
|
|
1149
|
+
};
|
|
1150
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
1151
|
+
if (rest[i] !== "--calls") continue;
|
|
1152
|
+
const callsPath = rest[i + 1];
|
|
1153
|
+
if (!callsPath) throw new Error("--calls needs a path");
|
|
1154
|
+
const raw = JSON.parse(readFileSync(resolve(process.cwd(), callsPath), "utf8"));
|
|
1155
|
+
if (Array.isArray(raw)) {
|
|
1156
|
+
for (const entry of raw as Array<{ path: string; dealId?: string }>) addParsedCall(entry.path, entry.dealId);
|
|
1157
|
+
} else {
|
|
1158
|
+
addParsedCall(callsPath);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const priorLabel = option(rest, "--prior-run");
|
|
1163
|
+
const priorSet = priorLabel ? await store.get(priorLabel) : null;
|
|
1164
|
+
if (priorLabel && !priorSet) throw new Error(`No observation run "${priorLabel}" for URGENT drift`);
|
|
1165
|
+
|
|
1166
|
+
const stats = computeOverlayStats(config, snapshot, documents);
|
|
1167
|
+
const directives = computeDirectives(config, set, stats, {
|
|
1168
|
+
minMentions: numericOption(rest, "--min-mentions") ?? undefined,
|
|
1169
|
+
promoteLift: numericOption(rest, "--promote-lift") ?? undefined,
|
|
1170
|
+
priorSet: priorSet ?? undefined,
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
if (rest.includes("--json")) {
|
|
1174
|
+
console.log(JSON.stringify({ stats, directives }, null, 2));
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
console.log(overlayToMarkdown(stats, directives));
|
|
1178
|
+
|
|
1179
|
+
if (rest.includes("--save")) {
|
|
1180
|
+
const taskAccount = option(rest, "--task-account");
|
|
1181
|
+
const taskDeal = option(rest, "--task-deal");
|
|
1182
|
+
if (!taskAccount && !taskDeal) {
|
|
1183
|
+
throw new Error(
|
|
1184
|
+
"--save needs --task-account <id> or --task-deal <id>: directives become approval-gated create_task operations, and the CRM needs a record to hang them on (your own company's account record works well)",
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
const plan = directivesToPlan(
|
|
1188
|
+
config,
|
|
1189
|
+
set,
|
|
1190
|
+
directives,
|
|
1191
|
+
taskDeal ? { objectType: "deal", objectId: taskDeal } : { objectType: "account", objectId: taskAccount as string },
|
|
1192
|
+
);
|
|
1193
|
+
const stored = await createFilePlanStore().save(plan);
|
|
1194
|
+
console.log(`Saved plan ${stored.plan.id} (${directives.length} directive task(s); approve via \`plans approve\`)`);
|
|
1195
|
+
}
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1092
1199
|
throw new Error(
|
|
1093
|
-
`Unknown market subcommand: ${subcommand} (try: init, capture, classify, worksheet, observe, fronts, axes, report, refresh)`,
|
|
1200
|
+
`Unknown market subcommand: ${subcommand} (try: init, capture, classify, worksheet, observe, fronts, axes, overlay, scale, report, refresh)`,
|
|
1094
1201
|
);
|
|
1095
1202
|
}
|
|
1096
1203
|
|
package/src/index.ts
CHANGED
|
@@ -161,6 +161,7 @@ export {
|
|
|
161
161
|
type ObservationConfidence,
|
|
162
162
|
type ObservationSet,
|
|
163
163
|
type ObservationStore,
|
|
164
|
+
type ScaleSignal,
|
|
164
165
|
type SpanVerificationFailure,
|
|
165
166
|
} from "./market.ts";
|
|
166
167
|
export {
|
|
@@ -182,6 +183,21 @@ export {
|
|
|
182
183
|
type ClassifyMarketResult,
|
|
183
184
|
type MarketWorksheet,
|
|
184
185
|
} from "./marketClassify.ts";
|
|
186
|
+
export {
|
|
187
|
+
computeDirectives,
|
|
188
|
+
computeOverlayStats,
|
|
189
|
+
directivesToPlan,
|
|
190
|
+
overlayToMarkdown,
|
|
191
|
+
type CallDocument,
|
|
192
|
+
type ClaimMentionStats,
|
|
193
|
+
type DirectiveStat,
|
|
194
|
+
type DirectiveType,
|
|
195
|
+
type MarketDirective,
|
|
196
|
+
type OverlayOptions,
|
|
197
|
+
type OverlayStats,
|
|
198
|
+
type VendorMentionStats,
|
|
199
|
+
} from "./marketOverlay.ts";
|
|
200
|
+
export { computeScaleIndex, scaleReportToText, type ScaleReport, type VendorScale } from "./marketScale.ts";
|
|
185
201
|
export { marketMapToHtml, marketMapToMarkdown } from "./marketReport.ts";
|
|
186
202
|
export { suggestValues, type SuggestionConfidence, type ValueSuggestion } from "./suggest.ts";
|
|
187
203
|
export type {
|
package/src/market.ts
CHANGED
|
@@ -38,6 +38,31 @@ export type MarketClaim = {
|
|
|
38
38
|
pricingStructure: string;
|
|
39
39
|
/** Operational definition: how a reader judges LOUD vs QUIET vs ABSENT. */
|
|
40
40
|
definition: string;
|
|
41
|
+
/**
|
|
42
|
+
* Exact terms buyers use for this claim, for deterministic mention
|
|
43
|
+
* matching against call transcripts (the overlay). No terms = no mention
|
|
44
|
+
* stats for this claim; matching is word-boundary, case-insensitive.
|
|
45
|
+
*/
|
|
46
|
+
terms?: string[];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* One public, citable scale signal for a vendor (G2 review count, LinkedIn
|
|
51
|
+
* headcount, disclosed revenue, self-reported customer count). The composite
|
|
52
|
+
* of several biased-in-different-directions signals sizes the report's
|
|
53
|
+
* bubbles — a RELATIVE scale index within the mapped set, never "market
|
|
54
|
+
* share" unqualified.
|
|
55
|
+
*/
|
|
56
|
+
export type ScaleSignal = {
|
|
57
|
+
/** e.g. "g2_reviews", "linkedin_employees", "revenue_usd", "self_reported_customers". */
|
|
58
|
+
metric: string;
|
|
59
|
+
value: number;
|
|
60
|
+
unit: string;
|
|
61
|
+
sourceUrl: string;
|
|
62
|
+
/** Verbatim snippet containing the number — same evidence posture as observations. */
|
|
63
|
+
quote: string;
|
|
64
|
+
asOf: string;
|
|
65
|
+
caveat?: string;
|
|
41
66
|
};
|
|
42
67
|
|
|
43
68
|
export type MarketVendor = {
|
|
@@ -49,6 +74,10 @@ export type MarketVendor = {
|
|
|
49
74
|
pricing: string | null;
|
|
50
75
|
product: string[];
|
|
51
76
|
};
|
|
77
|
+
/** Alternate names/spellings for deterministic mention matching. */
|
|
78
|
+
aliases?: string[];
|
|
79
|
+
/** Public scale signals; see ScaleSignal. */
|
|
80
|
+
scaleSignals?: ScaleSignal[];
|
|
52
81
|
notes?: string;
|
|
53
82
|
};
|
|
54
83
|
|