fullstackgtm 0.21.2 → 0.23.1

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
@@ -114,11 +114,40 @@ function suggestDealAccount(
114
114
  return { ...base, suggestedValue: null, confidence: "none", reason: "Deal not found in the snapshot." };
115
115
  }
116
116
 
117
- // Convention: "Contact Name - Company Name". Both signals below are
118
- // independent; agreement upgrades confidence, conflict downgrades it.
119
- const separatorIndex = deal.name.indexOf(" - ");
120
- const left = separatorIndex >= 0 ? deal.name.slice(0, separatorIndex) : deal.name;
121
- const right = separatorIndex >= 0 ? deal.name.slice(separatorIndex + 3).trim() : "";
117
+ // Convention 1: "Contact Name - Company Name" (hyphen, en or em dash).
118
+ // Both signals below are independent; agreement upgrades confidence,
119
+ // conflict downgrades it.
120
+ const separator = [" - ", " – ", " "]
121
+ .map((s) => ({ s, index: deal.name.indexOf(s) }))
122
+ .filter(({ index }) => index >= 0)
123
+ .sort((a, b) => a.index - b.index)[0];
124
+ const left = separator ? deal.name.slice(0, separator.index).trim() : deal.name;
125
+ const right = separator ? deal.name.slice(separator.index + separator.s.length).trim() : "";
126
+
127
+ // Convention 2: "Company - Deal descriptor" (e.g. "Globex – Expansion",
128
+ // "Hooli – New Business"). When the right side is purely deal-descriptor
129
+ // words, the company is on the LEFT. Only an exact account-name match
130
+ // counts as high; an unknown left side proposes a create — the engine
131
+ // never guesses at an existing record.
132
+ if (left && right && isDealDescriptor(right)) {
133
+ const leftMatch = accountsByNorm.get(normalize(left));
134
+ if (leftMatch) {
135
+ return {
136
+ ...base,
137
+ suggestedValue: leftMatch.id,
138
+ confidence: "high",
139
+ reason: `Deal name leads with the exact account name "${leftMatch.name}" ("${right}" is a deal descriptor, not a company).`,
140
+ };
141
+ }
142
+ if (!contactsByName.get(normalize(left))) {
143
+ return {
144
+ ...base,
145
+ suggestedValue: `create:${left}`,
146
+ confidence: "create",
147
+ reason: `No account named "${left}" exists ("${right}" is a deal descriptor) — approving creates the company, then links.`,
148
+ };
149
+ }
150
+ }
122
151
 
123
152
  // Signal 1: company-name match against account names.
124
153
  let nameMatch: { id: string; name: string } | null = null;
@@ -189,6 +218,22 @@ function suggestDealAccount(
189
218
  reason: `No account named "${right}" exists and contact "${left}" has none — approving creates the company, then links.`,
190
219
  };
191
220
  }
221
+ // Convention 3: "<Company> <descriptor…>" without a separator (e.g.
222
+ // "INITECH renewal"): strip trailing descriptor words and accept only an
223
+ // exact account-name match on what remains.
224
+ if (!separator) {
225
+ const words = normalize(deal.name).split(" ").filter(Boolean);
226
+ while (words.length > 1 && DEAL_DESCRIPTOR_WORDS.has(words[words.length - 1])) words.pop();
227
+ const strippedMatch = words.length > 0 ? accountsByNorm.get(words.join(" ")) : undefined;
228
+ if (strippedMatch) {
229
+ return {
230
+ ...base,
231
+ suggestedValue: strippedMatch.id,
232
+ confidence: "high",
233
+ reason: `Deal name leads with the exact account name "${strippedMatch.name}" (trailing words are deal descriptors).`,
234
+ };
235
+ }
236
+ }
192
237
  return {
193
238
  ...base,
194
239
  suggestedValue: null,
@@ -199,6 +244,25 @@ function suggestDealAccount(
199
244
  };
200
245
  }
201
246
 
247
+ /**
248
+ * Words that describe the deal rather than name a company. Used to recognize
249
+ * the "Company - Deal descriptor" naming convention: a segment counts as a
250
+ * descriptor only when EVERY word is in this list, so any real company name
251
+ * ("Brand New Startup") falls through to the contact/company conventions.
252
+ */
253
+ const DEAL_DESCRIPTOR_WORDS = new Set([
254
+ "renewal", "expansion", "pilot", "annual", "monthly", "platform", "new", "business",
255
+ "add", "on", "addon", "upsell", "upgrade", "trial", "poc", "proof", "concept",
256
+ "subscription", "license", "licence", "contract", "opportunity", "deal", "engagement",
257
+ "implementation", "onboarding", "services", "inbound", "outbound",
258
+ "q1", "q2", "q3", "q4",
259
+ ]);
260
+
261
+ function isDealDescriptor(segment: string): boolean {
262
+ const words = normalize(segment).split(" ").filter(Boolean);
263
+ return words.length > 0 && words.every((word) => DEAL_DESCRIPTOR_WORDS.has(word));
264
+ }
265
+
202
266
  /**
203
267
  * Survivor selection for merge_records. Ranking is deterministic and
204
268
  * evidence-based: most complete record first (count of populated canonical