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/dist/marketReport.js
CHANGED
|
@@ -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}">← ${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)} →</text>
|
|
114
|
+
<text class="ax-label" style="font-size:${fsAx}px" x="${PAD}" y="${PAD - 10}">↑ ${e(ay.positivePole)}${ay.signed ? ` · ↓ ${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)} × ${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=½) 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"
|
|
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 =
|
|
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 =
|
|
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`, `
|
|
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`)
|
|
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.
|
|
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",
|