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.
@@ -1,4 +1,5 @@
1
1
  import { computeFrontStates } from "./market.js";
2
+ import { assessAxes, messageBreadth } from "./marketAxes.js";
2
3
  /**
3
4
  * Render a market map as a client-ready deliverable: markdown for terminals
4
5
  * and PRs, and a self-contained printable HTML "field report" — front
@@ -77,6 +78,107 @@ export function marketMapToMarkdown(config, set) {
77
78
  }
78
79
  return `${lines.join("\n")}\n`;
79
80
  }
81
+ function svgScatter(points, ax, ay, anchor, mini) {
82
+ const W = mini ? 330 : 700;
83
+ const H = mini ? 250 : 460;
84
+ const PAD = mini ? 34 : 56;
85
+ const range = (axis, values) => {
86
+ if (axis.signed)
87
+ return [-1.1, 1.1];
88
+ if (values.length === 0)
89
+ return [0, 1];
90
+ return [Math.min(0, Math.min(...values) - 0.05), Math.max(...values) + 0.08];
91
+ };
92
+ const [xLo, xHi] = range(ax, points.map((p) => p.x));
93
+ const [yLo, yHi] = range(ay, points.map((p) => p.y));
94
+ const sx = (x) => PAD + ((x - xLo) / (xHi - xLo)) * (W - 2 * PAD);
95
+ const sy = (y) => H - PAD - ((y - yLo) / (yHi - yLo)) * (H - 2 * PAD);
96
+ const fsLabel = mini ? 8.5 : 10.5;
97
+ const fsAx = mini ? 8 : 10;
98
+ const e = escapeHtml;
99
+ const dots = points
100
+ .map((p) => {
101
+ const r = mini ? 3 + p.loud * 0.8 : 6 + p.loud * 1.6;
102
+ const cls = p.vendorId === anchor ? "dot-anchor" : "dot";
103
+ return (`<circle class="${cls}" cx="${sx(p.x).toFixed(1)}" cy="${sy(p.y).toFixed(1)}" r="${r.toFixed(1)}"/>` +
104
+ `<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>`);
105
+ })
106
+ .join("");
107
+ const midX = ax.signed ? `<line class="axis-mid" x1="${sx(0).toFixed(0)}" y1="${PAD}" x2="${sx(0).toFixed(0)}" y2="${H - PAD}"/>` : "";
108
+ const midY = ay.signed ? `<line class="axis-mid" x1="${PAD}" y1="${sy(0).toFixed(0)}" x2="${W - PAD}" y2="${sy(0).toFixed(0)}"/>` : "";
109
+ return `<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${e(ax.label)} vs ${e(ay.label)}">
110
+ <line class="axis" x1="${PAD}" y1="${H - PAD}" x2="${W - PAD}" y2="${H - PAD}"/>
111
+ <line class="axis" x1="${PAD}" y1="${PAD}" x2="${PAD}" y2="${H - PAD}"/>${midX}${midY}
112
+ <text class="ax-label" style="font-size:${fsAx}px" x="${PAD}" y="${H - 14}">&#8592; ${e(ax.negativePole)}</text>
113
+ <text class="ax-label" style="font-size:${fsAx}px" x="${W - PAD}" y="${H - 14}" text-anchor="end">${e(ax.positivePole)} &#8594;</text>
114
+ <text class="ax-label" style="font-size:${fsAx}px" x="${PAD}" y="${PAD - 10}">&#8593; ${e(ay.positivePole)}${ay.signed ? ` &#183; &#8595; ${e(ay.negativePole)}` : ""}</text>
115
+ ${dots}</svg>`;
116
+ }
117
+ function axisSectionsHtml(config, set) {
118
+ const axes = config.axes ?? [];
119
+ if (axes.length === 0)
120
+ return { strategicMap: "", report: null };
121
+ const e = escapeHtml;
122
+ const report = assessAxes(config, set);
123
+ const vendorNames = new Map(config.vendors.map((vendor) => [vendor.id, vendor.name]));
124
+ const loudCounts = new Map(report.vendors.map((vendorId) => [vendorId, messageBreadth(vendorId, set.observations).loudCount]));
125
+ const breadthAxis = {
126
+ id: "breadth",
127
+ label: "Message breadth",
128
+ negativePole: "FOCUSED",
129
+ positivePole: "BROAD (share of claims voiced)",
130
+ signed: false,
131
+ };
132
+ const axisInfo = new Map([
133
+ ...axes.map((axis) => [axis.id, { id: axis.id, label: axis.label, negativePole: axis.negativePole, positivePole: axis.positivePole, signed: true }]),
134
+ [breadthAxis.id, breadthAxis],
135
+ ]);
136
+ const positions = new Map();
137
+ for (const assessment of report.assessments) {
138
+ positions.set(assessment.axis.id, new Map(assessment.positions
139
+ .filter((entry) => entry.position !== null)
140
+ .map((entry) => [entry.vendorId, entry.position])));
141
+ }
142
+ const breadthMap = new Map();
143
+ for (const vendorId of report.vendors) {
144
+ const { breadth } = messageBreadth(vendorId, set.observations);
145
+ if (breadth !== null)
146
+ breadthMap.set(vendorId, breadth);
147
+ }
148
+ positions.set("breadth", breadthMap);
149
+ const pointsFor = (xId, yId) => {
150
+ const xs = positions.get(xId);
151
+ const ys = positions.get(yId);
152
+ if (!xs || !ys)
153
+ return [];
154
+ return report.vendors
155
+ .filter((vendorId) => xs.has(vendorId) && ys.has(vendorId))
156
+ .map((vendorId) => ({
157
+ vendorId,
158
+ name: vendorNames.get(vendorId) ?? vendorId,
159
+ x: xs.get(vendorId),
160
+ y: ys.get(vendorId),
161
+ loud: loudCounts.get(vendorId) ?? 0,
162
+ }));
163
+ };
164
+ const [px, py] = config.primaryAxes ?? [axes[0].id, axes[1]?.id ?? "breadth"];
165
+ const axInfo = axisInfo.get(px);
166
+ const ayInfo = axisInfo.get(py);
167
+ const statusOf = (id) => axes.find((axis) => axis.id === id)?.status ?? (id === "breadth" ? "derived" : "");
168
+ const strategicMap = `<section>
169
+ <h2><span class="no">03</span> Strategic map — ${e(axInfo.label)} &#215; ${e(ayInfo.label)}</h2>
170
+ <figure>${svgScatter(pointsFor(px, py), axInfo, ayInfo, config.anchorVendor, false)}
171
+ <figcaption>Positions computed from run ${e(set.runLabel)} observations: each axis is a per-claim scoring rubric
172
+ in the market config; a vendor sits at the intensity-weighted mean (loud=1, quiet=&#189;) of the claims it
173
+ voices. Dot size = LOUD count. Axis status — ${e(axInfo.label)}: ${e(statusOf(px))}; ${e(ayInfo.label)}: ${e(statusOf(py))}.</figcaption>
174
+ </figure>
175
+ </section>`;
176
+ // Deliberately no axis-pairing gallery here: the report is the client-facing
177
+ // artifact, best foot forward — one earned 2x2. Axis exploration (PCA,
178
+ // triangulation, the orthogonality screen over every pairing) lives in
179
+ // `market axes` for the analyst or agent doing the iterating.
180
+ return { strategicMap, report };
181
+ }
80
182
  export function marketMapToHtml(config, set) {
81
183
  const model = buildModel(config, set);
82
184
  const stateByClaim = new Map(model.fronts.map((front) => [front.claimId, front.state]));
@@ -87,6 +189,8 @@ export function marketMapToHtml(config, set) {
87
189
  const unobservable = set.observations.filter((obs) => obs.intensity === "unobservable").length;
88
190
  const anchor = config.anchorVendor;
89
191
  const e = escapeHtml;
192
+ const axisHtml = axisSectionsHtml(config, set);
193
+ const appendixNo = axisHtml.report ? "04" : "03";
90
194
  const matrixRows = model.orderedClaimIds
91
195
  .map((claimId) => {
92
196
  const claim = claimsById.get(claimId);
@@ -184,6 +288,14 @@ tr.front-open th .claim-cap { color:var(--accent); font-weight:600; }
184
288
  .ev-head { font-size:10.5px; letter-spacing:.1em; color:var(--accent); }
185
289
  .ev blockquote { font-style:italic; margin:6px 0; font-size:13.5px; line-height:1.5; }
186
290
  .ev-src { font-size:10px; color:var(--ink-soft); word-break:break-all; }
291
+ figure { margin-top:22px; border:1px solid var(--line); background:rgba(255,255,255,.35); }
292
+ .axis { stroke:var(--ink); stroke-width:1.5; }
293
+ .axis-mid { stroke:var(--line); stroke-dasharray:3 5; }
294
+ .ax-label { letter-spacing:.16em; fill:var(--ink-soft); font-family:"SF Mono",Menlo,Consolas,monospace; }
295
+ .dot { fill:rgba(33,29,22,.78); }
296
+ .dot-anchor { fill:var(--green); stroke:var(--ink); stroke-width:1.5; }
297
+ .dot-label { fill:var(--ink); text-anchor:middle; letter-spacing:.04em; font-family:"SF Mono",Menlo,Consolas,monospace; }
298
+ 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; }
187
299
  footer { margin-top:72px; border-top:3px double var(--ink); padding-top:14px; font-size:11px; color:var(--ink-soft);
188
300
  display:flex; justify-content:space-between; gap:20px; flex-wrap:wrap; }
189
301
  @media print { body { max-width:none; padding:0 8mm; background:white; } section { break-inside:avoid-page; } tr { break-inside:avoid; } }
@@ -221,8 +333,9 @@ footer { margin-top:72px; border-top:3px double var(--ink); padding-top:14px; fo
221
333
  <tbody>${matrixRows}</tbody>
222
334
  </table>
223
335
  </section>
336
+ ${axisHtml.strategicMap}
224
337
  <section>
225
- <h2><span class="no">03</span> Evidence appendix</h2>
338
+ <h2><span class="no">${appendixNo}</span> Evidence appendix</h2>
226
339
  ${appendix}
227
340
  </section>
228
341
  <footer>
package/dist/mcp.js CHANGED
@@ -260,7 +260,7 @@ export async function startMcpServer() {
260
260
  captureRun: z.string().optional(),
261
261
  },
262
262
  }, async ({ vendorId, configPath, captureRun }) => {
263
- const config = loadMarketConfig(resolve(process.cwd(), configPath ?? "market.config.json"));
263
+ const config = loadMarketConfigOrHint(resolve(process.cwd(), configPath ?? "market.config.json"));
264
264
  return content(buildWorksheet(config, vendorId, { captureRun }));
265
265
  });
266
266
  server.registerTool("fullstackgtm_market_observe", {
@@ -274,7 +274,7 @@ export async function startMcpServer() {
274
274
  configPath: z.string().optional().describe("Path to market.config.json (default ./market.config.json)"),
275
275
  },
276
276
  }, async ({ observationsPath, configPath }) => {
277
- const config = loadMarketConfig(resolve(process.cwd(), configPath ?? "market.config.json"));
277
+ const config = loadMarketConfigOrHint(resolve(process.cwd(), configPath ?? "market.config.json"));
278
278
  const set = JSON.parse(readFileSync(resolve(process.cwd(), observationsPath), "utf8"));
279
279
  const problems = validateObservationSet(config, set);
280
280
  const failures = verifyEvidenceSpans(set.observations, loadCaptureTexts(config.category).textByHash);
@@ -292,3 +292,14 @@ export async function startMcpServer() {
292
292
  const transport = new StdioServerTransport();
293
293
  await server.connect(transport);
294
294
  }
295
+ function loadMarketConfigOrHint(path) {
296
+ try {
297
+ return loadMarketConfig(path);
298
+ }
299
+ catch (error) {
300
+ if (error.code === "ENOENT") {
301
+ throw new Error(`No market config at ${path} — run \`fullstackgtm market init --category <name>\` in that directory first, or pass configPath.`);
302
+ }
303
+ throw error;
304
+ }
305
+ }
package/dist/types.d.ts CHANGED
@@ -222,6 +222,23 @@ export type PatchOperation = {
222
222
  evidenceIds?: string[];
223
223
  findingIds?: string[];
224
224
  verification?: PatchVerification;
225
+ /**
226
+ * Compare-and-set guards beyond the written field: each precondition is
227
+ * re-read at apply time and a mismatch turns the operation into a
228
+ * conflict instead of a write. Guards against a record drifting on a
229
+ * DIFFERENT field than the one being written (e.g. stage changed while
230
+ * an owner write was pending).
231
+ */
232
+ preconditions?: Array<{
233
+ field: string;
234
+ expectedValue: unknown;
235
+ }>;
236
+ /**
237
+ * Operations sharing a groupId are all-or-nothing at apply time: a
238
+ * conflict (beforeValue or precondition) on any member skips every
239
+ * member of the group.
240
+ */
241
+ groupId?: string;
225
242
  };
226
243
  /**
227
244
  * A patch plan is always a dry-run proposal. Applying a plan never mutates
@@ -238,6 +255,33 @@ export type PatchPlan = {
238
255
  pipelineFindings?: PipelineFinding[];
239
256
  evidence?: GtmEvidence[];
240
257
  operations: PatchOperation[];
258
+ /**
259
+ * The filter this plan's operations were selected by. Re-evaluated per
260
+ * record against a FRESH snapshot at apply time: any operation whose
261
+ * record no longer matches is reported as a conflict instead of applied.
262
+ * Unlike per-operation preconditions, this enforces the FULL filter —
263
+ * negations and relational pseudo-fields included.
264
+ */
265
+ filter?: {
266
+ objectType: "account" | "contact" | "deal";
267
+ where: string[];
268
+ };
269
+ /**
270
+ * Plan-level guards re-evaluated against a FRESH snapshot at apply time.
271
+ * If any guard fails, NO operation in the plan is applied. This is how a
272
+ * plan expresses cross-record eligibility ("apply only while the account
273
+ * still has no open deal in contractsent") that per-operation
274
+ * preconditions cannot reach.
275
+ */
276
+ guards?: PlanGuard[];
277
+ };
278
+ export type PlanGuard = {
279
+ objectType: "account" | "contact" | "deal";
280
+ /** filter expressions in bulk-update --where grammar, AND-ed */
281
+ where: string[];
282
+ /** none: guard passes when ZERO records match; some: when at least one matches */
283
+ expect: "none" | "some";
284
+ description?: string;
241
285
  };
242
286
  /** Pre-computed lookups shared by all rules so each rule stays O(n). */
243
287
  export type GtmSnapshotIndex = {
package/docs/api.md CHANGED
@@ -58,7 +58,9 @@ release.
58
58
  ## CLI
59
59
 
60
60
  Commands: `login` / `logout`, `snapshot`, `audit`, `report`, `diff`, `merge`, `plans`,
61
- `apply`, `rules`, `profiles`, `doctor`.
61
+ `apply`, `suggest`, `call` (`parse` / `score` / `link` / `plan`), `resolve`,
62
+ `market` (`init` / `capture` / `classify` / `worksheet` / `observe` / `fronts` /
63
+ `axes` / `report` / `refresh`), `rules`, `profiles`, `doctor`.
62
64
  Exit codes: `0` success · `1` error · `2` findings/regressions at the requested gate
63
65
  (`--fail-on`, `--fail-on-new-findings`). `--json` everywhere; JSON output shapes are stable.
64
66
 
@@ -78,7 +80,32 @@ deliverable in markdown or self-contained HTML: severity counts, prose summary,
78
80
  per-rule detail with capped examples, and next steps. `auditReportToMarkdown` /
79
81
  `auditReportToHtml` expose the same rendering programmatically.
80
82
 
83
+ ## Market map
84
+
85
+ Newer surface (0.16–0.18); shapes are settling toward the 1.0 contract. A live
86
+ model of the competitive category: claim taxonomy + vendor registry as a
87
+ reviewable `market.config.json` (`MarketConfig`, `MarketClaim`, `MarketVendor`,
88
+ `MarketAxis`), content-addressed page captures (`captureMarket`,
89
+ `loadCaptureTexts`), append-only observations (`ObservationSet`,
90
+ `MarketObservation`, `ObservationStore` / `createFileObservationStore` —
91
+ profile-scoped under `<home>/market/<category>`), and deterministic
92
+ derivations: `computeFrontStates` / `diffFrontStates` (front rule v1),
93
+ `assessAxes` / `pcaTop2` / `axisPosition` (axis discovery), and
94
+ `marketMapToMarkdown` / `marketMapToHtml` (the field report; renders the
95
+ primary strategic 2×2 when `axes` / `primaryAxes` are configured).
96
+
97
+ Intensity readings are proposals: `classifyMarket` (LLM, bring-your-own-key,
98
+ provenance-marked) or `buildWorksheet` + `market observe` (agent/human). Every
99
+ quoted evidence span is mechanically verified verbatim
100
+ (`verifyEvidenceSpans`; whitespace and punctuation-spacing normalized) against
101
+ the stored capture it cites before a set is accepted; failed captures read as
102
+ `unobservable`, never `absent`.
103
+
81
104
  ## MCP
82
105
 
83
106
  Tools: `fullstackgtm_audit`, `fullstackgtm_rules`, `fullstackgtm_apply`
84
- (requires explicit `approvedOperationIds`). Input schemas are stable.
107
+ (requires explicit `approvedOperationIds`), `fullstackgtm_suggest`,
108
+ `fullstackgtm_call_parse`, `fullstackgtm_resolve`,
109
+ `fullstackgtm_market_worksheet`, `fullstackgtm_market_observe` (validates,
110
+ verifies quoted spans against the stored captures, appends, returns front
111
+ states). Input schemas are stable.
package/llms.txt CHANGED
@@ -31,6 +31,22 @@ coaching scorecards; `call link` suggests the deal with confidence + reason;
31
31
  `call plan` proposes governed next-step writes through the standard
32
32
  approve/apply lifecycle.
33
33
 
34
+ ## Key invariants (market map)
35
+
36
+ `fullstackgtm market` models the competitive category: vendors + claim
37
+ taxonomy in `market.config.json`; `capture` stores vendor pages
38
+ content-addressed; `classify` (BYO key, same ladder as calls) or
39
+ `worksheet` + `observe` (agent/human channel) propose LOUD/QUIET/ABSENT
40
+ intensity readings per vendor × claim. Every quoted evidence span is
41
+ mechanically verified verbatim against the stored capture it cites;
42
+ unverifiable quotes are rejected (`--unverified` only when captures live
43
+ elsewhere). Failed captures read UNOBSERVABLE, never ABSENT. `fronts --diff`
44
+ = deterministic front states + drift between runs; `axes` = PCA axis
45
+ discovery + orthogonality screen; `report` = self-contained HTML field
46
+ report; `refresh` = capture → classify → drift → report in one command.
47
+ Storage is profile-scoped under `<home>/market/<category>`. MCP:
48
+ `fullstackgtm_market_worksheet`, `fullstackgtm_market_observe`.
49
+
34
50
  ## Key invariants
35
51
 
36
52
  - Reads are safe by default; nothing is written without explicit `--approve`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullstackgtm",
3
- "version": "0.17.0",
3
+ "version": "0.19.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",