fullstackgtm 0.17.0 → 0.19.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 +72 -0
- package/INSTALL_FOR_AGENTS.md +10 -5
- package/README.md +17 -0
- package/dist/bulkUpdate.d.ts +37 -0
- package/dist/bulkUpdate.js +315 -0
- package/dist/cli.js +93 -2
- package/dist/connector.d.ts +6 -0
- package/dist/connector.js +158 -17
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/market.d.ts +16 -0
- package/dist/market.js +27 -0
- package/dist/marketAxes.d.ts +77 -0
- package/dist/marketAxes.js +199 -0
- package/dist/marketReport.js +114 -1
- package/dist/mcp.js +13 -2
- package/dist/types.d.ts +44 -0
- package/docs/api.md +29 -2
- package/llms.txt +16 -0
- package/package.json +1 -1
- package/src/bulkUpdate.ts +375 -0
- package/src/cli.ts +97 -2
- package/src/connector.ts +169 -23
- package/src/index.ts +15 -0
- package/src/market.ts +41 -0
- package/src/marketAxes.ts +268 -0
- package/src/marketReport.ts +134 -1
- package/src/mcp.ts +15 -2
- package/src/types.ts +39 -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
|
@@ -335,7 +335,7 @@ export async function startMcpServer() {
|
|
|
335
335
|
},
|
|
336
336
|
},
|
|
337
337
|
async ({ vendorId, configPath, captureRun }) => {
|
|
338
|
-
const config =
|
|
338
|
+
const config = loadMarketConfigOrHint(resolve(process.cwd(), configPath ?? "market.config.json"));
|
|
339
339
|
return content(buildWorksheet(config, vendorId, { captureRun }));
|
|
340
340
|
},
|
|
341
341
|
);
|
|
@@ -355,7 +355,7 @@ export async function startMcpServer() {
|
|
|
355
355
|
},
|
|
356
356
|
},
|
|
357
357
|
async ({ observationsPath, configPath }) => {
|
|
358
|
-
const config =
|
|
358
|
+
const config = loadMarketConfigOrHint(resolve(process.cwd(), configPath ?? "market.config.json"));
|
|
359
359
|
const set = JSON.parse(readFileSync(resolve(process.cwd(), observationsPath), "utf8")) as ObservationSet;
|
|
360
360
|
const problems = validateObservationSet(config, set);
|
|
361
361
|
const failures = verifyEvidenceSpans(set.observations, loadCaptureTexts(config.category).textByHash);
|
|
@@ -375,3 +375,16 @@ export async function startMcpServer() {
|
|
|
375
375
|
const transport = new StdioServerTransport();
|
|
376
376
|
await server.connect(transport);
|
|
377
377
|
}
|
|
378
|
+
|
|
379
|
+
function loadMarketConfigOrHint(path: string): ReturnType<typeof loadMarketConfig> {
|
|
380
|
+
try {
|
|
381
|
+
return loadMarketConfig(path);
|
|
382
|
+
} catch (error) {
|
|
383
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
384
|
+
throw new Error(
|
|
385
|
+
`No market config at ${path} — run \`fullstackgtm market init --category <name>\` in that directory first, or pass configPath.`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -289,6 +289,20 @@ export type PatchOperation = {
|
|
|
289
289
|
evidenceIds?: string[];
|
|
290
290
|
findingIds?: string[];
|
|
291
291
|
verification?: PatchVerification;
|
|
292
|
+
/**
|
|
293
|
+
* Compare-and-set guards beyond the written field: each precondition is
|
|
294
|
+
* re-read at apply time and a mismatch turns the operation into a
|
|
295
|
+
* conflict instead of a write. Guards against a record drifting on a
|
|
296
|
+
* DIFFERENT field than the one being written (e.g. stage changed while
|
|
297
|
+
* an owner write was pending).
|
|
298
|
+
*/
|
|
299
|
+
preconditions?: Array<{ field: string; expectedValue: unknown }>;
|
|
300
|
+
/**
|
|
301
|
+
* Operations sharing a groupId are all-or-nothing at apply time: a
|
|
302
|
+
* conflict (beforeValue or precondition) on any member skips every
|
|
303
|
+
* member of the group.
|
|
304
|
+
*/
|
|
305
|
+
groupId?: string;
|
|
292
306
|
};
|
|
293
307
|
|
|
294
308
|
/**
|
|
@@ -306,6 +320,31 @@ export type PatchPlan = {
|
|
|
306
320
|
pipelineFindings?: PipelineFinding[];
|
|
307
321
|
evidence?: GtmEvidence[];
|
|
308
322
|
operations: PatchOperation[];
|
|
323
|
+
/**
|
|
324
|
+
* The filter this plan's operations were selected by. Re-evaluated per
|
|
325
|
+
* record against a FRESH snapshot at apply time: any operation whose
|
|
326
|
+
* record no longer matches is reported as a conflict instead of applied.
|
|
327
|
+
* Unlike per-operation preconditions, this enforces the FULL filter —
|
|
328
|
+
* negations and relational pseudo-fields included.
|
|
329
|
+
*/
|
|
330
|
+
filter?: { objectType: "account" | "contact" | "deal"; where: string[] };
|
|
331
|
+
/**
|
|
332
|
+
* Plan-level guards re-evaluated against a FRESH snapshot at apply time.
|
|
333
|
+
* If any guard fails, NO operation in the plan is applied. This is how a
|
|
334
|
+
* plan expresses cross-record eligibility ("apply only while the account
|
|
335
|
+
* still has no open deal in contractsent") that per-operation
|
|
336
|
+
* preconditions cannot reach.
|
|
337
|
+
*/
|
|
338
|
+
guards?: PlanGuard[];
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
export type PlanGuard = {
|
|
342
|
+
objectType: "account" | "contact" | "deal";
|
|
343
|
+
/** filter expressions in bulk-update --where grammar, AND-ed */
|
|
344
|
+
where: string[];
|
|
345
|
+
/** none: guard passes when ZERO records match; some: when at least one matches */
|
|
346
|
+
expect: "none" | "some";
|
|
347
|
+
description?: string;
|
|
309
348
|
};
|
|
310
349
|
|
|
311
350
|
// ── Audit rule engine ──────────────────────────────────────────
|