fullstackgtm 0.16.0 → 0.18.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 +69 -0
- package/INSTALL_FOR_AGENTS.md +10 -5
- package/README.md +17 -0
- package/dist/cli.js +141 -12
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/llm.d.ts +7 -0
- package/dist/llm.js +7 -1
- package/dist/market.d.ts +35 -0
- package/dist/market.js +100 -0
- package/dist/marketAxes.d.ts +77 -0
- package/dist/marketAxes.js +199 -0
- package/dist/marketClassify.d.ts +49 -0
- package/dist/marketClassify.js +201 -0
- package/dist/marketReport.js +114 -1
- package/dist/mcp.js +45 -0
- package/docs/api.md +29 -2
- package/llms.txt +16 -0
- package/package.json +1 -1
- package/src/cli.ts +150 -12
- package/src/index.ts +24 -0
- package/src/llm.ts +7 -1
- package/src/market.ts +130 -0
- package/src/marketAxes.ts +268 -0
- package/src/marketClassify.ts +286 -0
- package/src/marketReport.ts +134 -1
- package/src/mcp.ts +65 -0
package/src/marketReport.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
ObservationSet,
|
|
7
7
|
} from "./market.ts";
|
|
8
8
|
import { computeFrontStates } from "./market.ts";
|
|
9
|
+
import { assessAxes, messageBreadth, type AxesReport } from "./marketAxes.ts";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Render a market map as a client-ready deliverable: markdown for terminals
|
|
@@ -104,6 +105,127 @@ export function marketMapToMarkdown(config: MarketConfig, set: ObservationSet):
|
|
|
104
105
|
return `${lines.join("\n")}\n`;
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
type ScatterPoint = { vendorId: string; name: string; x: number; y: number; loud: number };
|
|
109
|
+
type ScatterAxis = { label: string; negativePole: string; positivePole: string; signed: boolean };
|
|
110
|
+
|
|
111
|
+
function svgScatter(
|
|
112
|
+
points: ScatterPoint[],
|
|
113
|
+
ax: ScatterAxis,
|
|
114
|
+
ay: ScatterAxis,
|
|
115
|
+
anchor: string | undefined,
|
|
116
|
+
mini: boolean,
|
|
117
|
+
): string {
|
|
118
|
+
const W = mini ? 330 : 700;
|
|
119
|
+
const H = mini ? 250 : 460;
|
|
120
|
+
const PAD = mini ? 34 : 56;
|
|
121
|
+
const range = (axis: ScatterAxis, values: number[]): [number, number] => {
|
|
122
|
+
if (axis.signed) return [-1.1, 1.1];
|
|
123
|
+
if (values.length === 0) return [0, 1];
|
|
124
|
+
return [Math.min(0, Math.min(...values) - 0.05), Math.max(...values) + 0.08];
|
|
125
|
+
};
|
|
126
|
+
const [xLo, xHi] = range(ax, points.map((p) => p.x));
|
|
127
|
+
const [yLo, yHi] = range(ay, points.map((p) => p.y));
|
|
128
|
+
const sx = (x: number) => PAD + ((x - xLo) / (xHi - xLo)) * (W - 2 * PAD);
|
|
129
|
+
const sy = (y: number) => H - PAD - ((y - yLo) / (yHi - yLo)) * (H - 2 * PAD);
|
|
130
|
+
const fsLabel = mini ? 8.5 : 10.5;
|
|
131
|
+
const fsAx = mini ? 8 : 10;
|
|
132
|
+
const e = escapeHtml;
|
|
133
|
+
const dots = points
|
|
134
|
+
.map((p) => {
|
|
135
|
+
const r = mini ? 3 + p.loud * 0.8 : 6 + p.loud * 1.6;
|
|
136
|
+
const cls = p.vendorId === anchor ? "dot-anchor" : "dot";
|
|
137
|
+
return (
|
|
138
|
+
`<circle class="${cls}" cx="${sx(p.x).toFixed(1)}" cy="${sy(p.y).toFixed(1)}" r="${r.toFixed(1)}"/>` +
|
|
139
|
+
`<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>`
|
|
140
|
+
);
|
|
141
|
+
})
|
|
142
|
+
.join("");
|
|
143
|
+
const midX = ax.signed ? `<line class="axis-mid" x1="${sx(0).toFixed(0)}" y1="${PAD}" x2="${sx(0).toFixed(0)}" y2="${H - PAD}"/>` : "";
|
|
144
|
+
const midY = ay.signed ? `<line class="axis-mid" x1="${PAD}" y1="${sy(0).toFixed(0)}" x2="${W - PAD}" y2="${sy(0).toFixed(0)}"/>` : "";
|
|
145
|
+
return `<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${e(ax.label)} vs ${e(ay.label)}">
|
|
146
|
+
<line class="axis" x1="${PAD}" y1="${H - PAD}" x2="${W - PAD}" y2="${H - PAD}"/>
|
|
147
|
+
<line class="axis" x1="${PAD}" y1="${PAD}" x2="${PAD}" y2="${H - PAD}"/>${midX}${midY}
|
|
148
|
+
<text class="ax-label" style="font-size:${fsAx}px" x="${PAD}" y="${H - 14}">← ${e(ax.negativePole)}</text>
|
|
149
|
+
<text class="ax-label" style="font-size:${fsAx}px" x="${W - PAD}" y="${H - 14}" text-anchor="end">${e(ax.positivePole)} →</text>
|
|
150
|
+
<text class="ax-label" style="font-size:${fsAx}px" x="${PAD}" y="${PAD - 10}">↑ ${e(ay.positivePole)}${ay.signed ? ` · ↓ ${e(ay.negativePole)}` : ""}</text>
|
|
151
|
+
${dots}</svg>`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function axisSectionsHtml(
|
|
155
|
+
config: MarketConfig,
|
|
156
|
+
set: ObservationSet,
|
|
157
|
+
): { strategicMap: string; report: AxesReport | null } {
|
|
158
|
+
const axes = config.axes ?? [];
|
|
159
|
+
if (axes.length === 0) return { strategicMap: "", report: null };
|
|
160
|
+
const e = escapeHtml;
|
|
161
|
+
const report = assessAxes(config, set);
|
|
162
|
+
const vendorNames = new Map(config.vendors.map((vendor) => [vendor.id, vendor.name]));
|
|
163
|
+
const loudCounts = new Map(report.vendors.map((vendorId) => [vendorId, messageBreadth(vendorId, set.observations).loudCount]));
|
|
164
|
+
|
|
165
|
+
const breadthAxis: ScatterAxis & { id: string } = {
|
|
166
|
+
id: "breadth",
|
|
167
|
+
label: "Message breadth",
|
|
168
|
+
negativePole: "FOCUSED",
|
|
169
|
+
positivePole: "BROAD (share of claims voiced)",
|
|
170
|
+
signed: false,
|
|
171
|
+
};
|
|
172
|
+
const axisInfo = new Map<string, ScatterAxis & { id: string }>([
|
|
173
|
+
...axes.map((axis) => [axis.id, { id: axis.id, label: axis.label, negativePole: axis.negativePole, positivePole: axis.positivePole, signed: true }] as const),
|
|
174
|
+
[breadthAxis.id, breadthAxis],
|
|
175
|
+
]);
|
|
176
|
+
const positions = new Map<string, Map<string, number>>();
|
|
177
|
+
for (const assessment of report.assessments) {
|
|
178
|
+
positions.set(
|
|
179
|
+
assessment.axis.id,
|
|
180
|
+
new Map(
|
|
181
|
+
assessment.positions
|
|
182
|
+
.filter((entry): entry is { vendorId: string; position: number } => entry.position !== null)
|
|
183
|
+
.map((entry) => [entry.vendorId, entry.position]),
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
const breadthMap = new Map<string, number>();
|
|
188
|
+
for (const vendorId of report.vendors) {
|
|
189
|
+
const { breadth } = messageBreadth(vendorId, set.observations);
|
|
190
|
+
if (breadth !== null) breadthMap.set(vendorId, breadth);
|
|
191
|
+
}
|
|
192
|
+
positions.set("breadth", breadthMap);
|
|
193
|
+
|
|
194
|
+
const pointsFor = (xId: string, yId: string): ScatterPoint[] => {
|
|
195
|
+
const xs = positions.get(xId);
|
|
196
|
+
const ys = positions.get(yId);
|
|
197
|
+
if (!xs || !ys) return [];
|
|
198
|
+
return report.vendors
|
|
199
|
+
.filter((vendorId) => xs.has(vendorId) && ys.has(vendorId))
|
|
200
|
+
.map((vendorId) => ({
|
|
201
|
+
vendorId,
|
|
202
|
+
name: vendorNames.get(vendorId) ?? vendorId,
|
|
203
|
+
x: xs.get(vendorId) as number,
|
|
204
|
+
y: ys.get(vendorId) as number,
|
|
205
|
+
loud: loudCounts.get(vendorId) ?? 0,
|
|
206
|
+
}));
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const [px, py] = config.primaryAxes ?? [axes[0].id, axes[1]?.id ?? "breadth"];
|
|
210
|
+
const axInfo = axisInfo.get(px) as ScatterAxis & { id: string };
|
|
211
|
+
const ayInfo = axisInfo.get(py) as ScatterAxis & { id: string };
|
|
212
|
+
const statusOf = (id: string) => axes.find((axis) => axis.id === id)?.status ?? (id === "breadth" ? "derived" : "");
|
|
213
|
+
const strategicMap = `<section>
|
|
214
|
+
<h2><span class="no">03</span> Strategic map — ${e(axInfo.label)} × ${e(ayInfo.label)}</h2>
|
|
215
|
+
<figure>${svgScatter(pointsFor(px, py), axInfo, ayInfo, config.anchorVendor, false)}
|
|
216
|
+
<figcaption>Positions computed from run ${e(set.runLabel)} observations: each axis is a per-claim scoring rubric
|
|
217
|
+
in the market config; a vendor sits at the intensity-weighted mean (loud=1, quiet=½) of the claims it
|
|
218
|
+
voices. Dot size = LOUD count. Axis status — ${e(axInfo.label)}: ${e(statusOf(px))}; ${e(ayInfo.label)}: ${e(statusOf(py))}.</figcaption>
|
|
219
|
+
</figure>
|
|
220
|
+
</section>`;
|
|
221
|
+
|
|
222
|
+
// Deliberately no axis-pairing gallery here: the report is the client-facing
|
|
223
|
+
// artifact, best foot forward — one earned 2x2. Axis exploration (PCA,
|
|
224
|
+
// triangulation, the orthogonality screen over every pairing) lives in
|
|
225
|
+
// `market axes` for the analyst or agent doing the iterating.
|
|
226
|
+
return { strategicMap, report };
|
|
227
|
+
}
|
|
228
|
+
|
|
107
229
|
export function marketMapToHtml(config: MarketConfig, set: ObservationSet): string {
|
|
108
230
|
const model = buildModel(config, set);
|
|
109
231
|
const stateByClaim = new Map(model.fronts.map((front) => [front.claimId, front.state]));
|
|
@@ -113,6 +235,8 @@ export function marketMapToHtml(config: MarketConfig, set: ObservationSet): stri
|
|
|
113
235
|
const unobservable = set.observations.filter((obs) => obs.intensity === "unobservable").length;
|
|
114
236
|
const anchor = config.anchorVendor;
|
|
115
237
|
const e = escapeHtml;
|
|
238
|
+
const axisHtml = axisSectionsHtml(config, set);
|
|
239
|
+
const appendixNo = axisHtml.report ? "04" : "03";
|
|
116
240
|
|
|
117
241
|
const matrixRows = model.orderedClaimIds
|
|
118
242
|
.map((claimId) => {
|
|
@@ -223,6 +347,14 @@ tr.front-open th .claim-cap { color:var(--accent); font-weight:600; }
|
|
|
223
347
|
.ev-head { font-size:10.5px; letter-spacing:.1em; color:var(--accent); }
|
|
224
348
|
.ev blockquote { font-style:italic; margin:6px 0; font-size:13.5px; line-height:1.5; }
|
|
225
349
|
.ev-src { font-size:10px; color:var(--ink-soft); word-break:break-all; }
|
|
350
|
+
figure { margin-top:22px; border:1px solid var(--line); background:rgba(255,255,255,.35); }
|
|
351
|
+
.axis { stroke:var(--ink); stroke-width:1.5; }
|
|
352
|
+
.axis-mid { stroke:var(--line); stroke-dasharray:3 5; }
|
|
353
|
+
.ax-label { letter-spacing:.16em; fill:var(--ink-soft); font-family:"SF Mono",Menlo,Consolas,monospace; }
|
|
354
|
+
.dot { fill:rgba(33,29,22,.78); }
|
|
355
|
+
.dot-anchor { fill:var(--green); stroke:var(--ink); stroke-width:1.5; }
|
|
356
|
+
.dot-label { fill:var(--ink); text-anchor:middle; letter-spacing:.04em; font-family:"SF Mono",Menlo,Consolas,monospace; }
|
|
357
|
+
figcaption { font-size:12px; color:var(--ink-soft); padding:12px 16px 14px; font-style:italic; border-top:1px solid var(--line); line-height:1.5; }
|
|
226
358
|
footer { margin-top:72px; border-top:3px double var(--ink); padding-top:14px; font-size:11px; color:var(--ink-soft);
|
|
227
359
|
display:flex; justify-content:space-between; gap:20px; flex-wrap:wrap; }
|
|
228
360
|
@media print { body { max-width:none; padding:0 8mm; background:white; } section { break-inside:avoid-page; } tr { break-inside:avoid; } }
|
|
@@ -260,8 +392,9 @@ footer { margin-top:72px; border-top:3px double var(--ink); padding-top:14px; fo
|
|
|
260
392
|
<tbody>${matrixRows}</tbody>
|
|
261
393
|
</table>
|
|
262
394
|
</section>
|
|
395
|
+
${axisHtml.strategicMap}
|
|
263
396
|
<section>
|
|
264
|
-
<h2><span class="no"
|
|
397
|
+
<h2><span class="no">${appendixNo}</span> Evidence appendix</h2>
|
|
265
398
|
${appendix}
|
|
266
399
|
</section>
|
|
267
400
|
<footer>
|
package/src/mcp.ts
CHANGED
|
@@ -48,6 +48,16 @@ import { builtinAuditRules } from "./rules.ts";
|
|
|
48
48
|
import { sampleSnapshot } from "./sampleData.ts";
|
|
49
49
|
import { normalizeTranscript, parseCall } from "./calls.ts";
|
|
50
50
|
import { extractInsightsLlm, resolveLlmCredential } from "./llm.ts";
|
|
51
|
+
import {
|
|
52
|
+
computeFrontStates,
|
|
53
|
+
createFileObservationStore,
|
|
54
|
+
loadCaptureTexts,
|
|
55
|
+
loadMarketConfig,
|
|
56
|
+
validateObservationSet,
|
|
57
|
+
verifyEvidenceSpans,
|
|
58
|
+
type ObservationSet,
|
|
59
|
+
} from "./market.ts";
|
|
60
|
+
import { buildWorksheet } from "./marketClassify.ts";
|
|
51
61
|
import { resolveRecord } from "./resolve.ts";
|
|
52
62
|
import { suggestValues } from "./suggest.ts";
|
|
53
63
|
import type { CanonicalGtmSnapshot, GtmConnector, PatchPlan } from "./types.ts";
|
|
@@ -307,6 +317,61 @@ export async function startMcpServer() {
|
|
|
307
317
|
},
|
|
308
318
|
);
|
|
309
319
|
|
|
320
|
+
server.registerTool(
|
|
321
|
+
"fullstackgtm_market_worksheet",
|
|
322
|
+
{
|
|
323
|
+
title: "Market Map Classification Worksheet",
|
|
324
|
+
description:
|
|
325
|
+
"Get everything needed to classify ONE vendor's messaging intensity for a market map: " +
|
|
326
|
+
"the claim taxonomy with judging definitions, the surface rule, and the captured page " +
|
|
327
|
+
"texts. Read each claim's definition, judge loud/quiet/absent from the page texts only, " +
|
|
328
|
+
"and quote verbatim spans (≤300 chars) for every loud/quiet reading. Submit the full " +
|
|
329
|
+
"ObservationSet via fullstackgtm_market_observe — quotes are verified character-for-" +
|
|
330
|
+
"character against the captures, so never paraphrase.",
|
|
331
|
+
inputSchema: {
|
|
332
|
+
vendorId: z.string(),
|
|
333
|
+
configPath: z.string().optional().describe("Path to market.config.json (default ./market.config.json)"),
|
|
334
|
+
captureRun: z.string().optional(),
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
async ({ vendorId, configPath, captureRun }) => {
|
|
338
|
+
const config = loadMarketConfig(resolve(process.cwd(), configPath ?? "market.config.json"));
|
|
339
|
+
return content(buildWorksheet(config, vendorId, { captureRun }));
|
|
340
|
+
},
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
server.registerTool(
|
|
344
|
+
"fullstackgtm_market_observe",
|
|
345
|
+
{
|
|
346
|
+
title: "Submit Market Map Observations",
|
|
347
|
+
description:
|
|
348
|
+
"Submit a complete ObservationSet (every vendor × claim cell) for a market map run. " +
|
|
349
|
+
"Validates coverage, the verbatim-evidence rule, and mechanically verifies every quoted " +
|
|
350
|
+
"span against the stored capture it cites. Returns problems if rejected; nothing is " +
|
|
351
|
+
"stored unless the whole set passes. Observations are append-only — use a new runLabel.",
|
|
352
|
+
inputSchema: {
|
|
353
|
+
observationsPath: z.string().describe("Path to the ObservationSet JSON file"),
|
|
354
|
+
configPath: z.string().optional().describe("Path to market.config.json (default ./market.config.json)"),
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
async ({ observationsPath, configPath }) => {
|
|
358
|
+
const config = loadMarketConfig(resolve(process.cwd(), configPath ?? "market.config.json"));
|
|
359
|
+
const set = JSON.parse(readFileSync(resolve(process.cwd(), observationsPath), "utf8")) as ObservationSet;
|
|
360
|
+
const problems = validateObservationSet(config, set);
|
|
361
|
+
const failures = verifyEvidenceSpans(set.observations, loadCaptureTexts(config.category).textByHash);
|
|
362
|
+
if (problems.length > 0 || failures.length > 0) {
|
|
363
|
+
return content({
|
|
364
|
+
accepted: false,
|
|
365
|
+
problems,
|
|
366
|
+
spanFailures: failures.map((failure) => `${failure.vendorId} × ${failure.claimId}: ${failure.problem}`),
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
await createFileObservationStore(config.category).append(set);
|
|
370
|
+
const fronts = computeFrontStates(config, set);
|
|
371
|
+
return content({ accepted: true, runLabel: set.runLabel, observations: set.observations.length, fronts });
|
|
372
|
+
},
|
|
373
|
+
);
|
|
374
|
+
|
|
310
375
|
const transport = new StdioServerTransport();
|
|
311
376
|
await server.connect(transport);
|
|
312
377
|
}
|