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/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