fullstackgtm 0.11.0 → 0.12.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 +59 -0
- package/README.md +1 -1
- package/dist/connectors/hubspot.js +160 -11
- package/dist/connectors/salesforce.js +66 -9
- package/dist/merge.d.ts +1 -0
- package/dist/merge.js +1 -1
- package/dist/rules.js +23 -19
- package/dist/suggest.js +77 -0
- package/dist/types.d.ts +1 -1
- package/docs/api.md +1 -1
- package/docs/crm-health-lifecycle.md +135 -0
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/src/connectors/hubspot.ts +161 -11
- package/src/connectors/salesforce.ts +69 -9
- package/src/merge.ts +1 -1
- package/src/rules.ts +26 -19
- package/src/suggest.ts +88 -0
- package/src/types.ts +5 -1
package/src/suggest.ts
CHANGED
|
@@ -62,6 +62,11 @@ export function suggestValues(
|
|
|
62
62
|
continue;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
if (placeholder === "requires_human_survivor_selection") {
|
|
66
|
+
suggestions.push(suggestSurvivor(operation, snapshot, dealsById));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
65
70
|
if (placeholder === "requires_human_owner_selection" && activeUsers.length === 1) {
|
|
66
71
|
suggestions.push({
|
|
67
72
|
operationId: operation.id,
|
|
@@ -194,6 +199,89 @@ function suggestDealAccount(
|
|
|
194
199
|
};
|
|
195
200
|
}
|
|
196
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Survivor selection for merge_records. Ranking is deterministic and
|
|
204
|
+
* evidence-based: most complete record first (count of populated canonical
|
|
205
|
+
* fields), most recent activity as the tiebreaker, group order last.
|
|
206
|
+
* Confidence is capped at "low" by design: merges are irreversible, so a
|
|
207
|
+
* survivor suggestion must never clear the default bulk-approval bar —
|
|
208
|
+
* accepting one requires --min-confidence low or an explicit --value.
|
|
209
|
+
*/
|
|
210
|
+
function suggestSurvivor(
|
|
211
|
+
operation: PatchOperation,
|
|
212
|
+
snapshot: CanonicalGtmSnapshot,
|
|
213
|
+
dealsById: Map<string, { name: string }>,
|
|
214
|
+
): ValueSuggestion {
|
|
215
|
+
const base = {
|
|
216
|
+
operationId: operation.id,
|
|
217
|
+
objectType: operation.objectType,
|
|
218
|
+
objectId: operation.objectId,
|
|
219
|
+
objectName: dealsById.get(operation.objectId)?.name,
|
|
220
|
+
placeholder: "requires_human_survivor_selection",
|
|
221
|
+
};
|
|
222
|
+
const groupIds = Array.isArray(operation.beforeValue)
|
|
223
|
+
? operation.beforeValue.map((id) => String(id))
|
|
224
|
+
: [];
|
|
225
|
+
if (groupIds.length < 2) {
|
|
226
|
+
return {
|
|
227
|
+
...base,
|
|
228
|
+
suggestedValue: null,
|
|
229
|
+
confidence: "none",
|
|
230
|
+
reason: "Operation does not carry a duplicate group (expected ids in beforeValue).",
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const collection =
|
|
234
|
+
operation.objectType === "account"
|
|
235
|
+
? snapshot.accounts
|
|
236
|
+
: operation.objectType === "contact"
|
|
237
|
+
? snapshot.contacts
|
|
238
|
+
: operation.objectType === "deal"
|
|
239
|
+
? snapshot.deals
|
|
240
|
+
: [];
|
|
241
|
+
const records = groupIds
|
|
242
|
+
.map((id) => collection.find((row) => row.id === id))
|
|
243
|
+
.filter((row): row is NonNullable<typeof row> => row !== undefined);
|
|
244
|
+
if (records.length !== groupIds.length) {
|
|
245
|
+
return {
|
|
246
|
+
...base,
|
|
247
|
+
suggestedValue: null,
|
|
248
|
+
confidence: "none",
|
|
249
|
+
reason: "Not every group member is present in the snapshot; re-run the audit before merging.",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const ranked = [...records].sort((a, b) => {
|
|
253
|
+
const completeness = populatedFields(b) - populatedFields(a);
|
|
254
|
+
if (completeness !== 0) return completeness;
|
|
255
|
+
return activityMs(b) - activityMs(a);
|
|
256
|
+
});
|
|
257
|
+
const winner = ranked[0];
|
|
258
|
+
const runnerUp = ranked[1];
|
|
259
|
+
const name = "name" in winner && typeof winner.name === "string" ? winner.name : winner.id;
|
|
260
|
+
return {
|
|
261
|
+
...base,
|
|
262
|
+
suggestedValue: winner.id,
|
|
263
|
+
confidence: "low",
|
|
264
|
+
reason:
|
|
265
|
+
`"${name}" (${winner.id}) is the most complete record in the group ` +
|
|
266
|
+
`(${populatedFields(winner)} populated fields vs ${populatedFields(runnerUp)}` +
|
|
267
|
+
`${activityMs(winner) > activityMs(runnerUp) ? ", and more recent activity" : ""}). ` +
|
|
268
|
+
`Merging is IRREVERSIBLE — survivor suggestions never exceed low confidence; ` +
|
|
269
|
+
`approve with --min-confidence low or an explicit --value after review.`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function populatedFields(record: object) {
|
|
274
|
+
return Object.values(record).filter(
|
|
275
|
+
(value) => value !== undefined && value !== null && value !== "",
|
|
276
|
+
).length;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function activityMs(record: object) {
|
|
280
|
+
const value = (record as { lastActivityAt?: string }).lastActivityAt;
|
|
281
|
+
const parsed = value ? Date.parse(value) : NaN;
|
|
282
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
283
|
+
}
|
|
284
|
+
|
|
197
285
|
function normalize(value: string) {
|
|
198
286
|
return value
|
|
199
287
|
.toLowerCase()
|
package/src/types.ts
CHANGED
|
@@ -41,7 +41,11 @@ export type PatchOperationType =
|
|
|
41
41
|
| "clear_field"
|
|
42
42
|
| "link_record"
|
|
43
43
|
| "archive_record"
|
|
44
|
-
| "create_task"
|
|
44
|
+
| "create_task"
|
|
45
|
+
// Merge a duplicate group into a survivor. beforeValue is the group's
|
|
46
|
+
// record ids; afterValue is the survivor id (requires_human_survivor_selection
|
|
47
|
+
// until a human picks). IRREVERSIBLE on every provider that supports it.
|
|
48
|
+
| "merge_records";
|
|
45
49
|
|
|
46
50
|
export type AuditFindingSeverity = "info" | "warning" | "critical";
|
|
47
51
|
|