fullstackgtm 0.46.0 → 0.48.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 +44 -6
- package/INSTALL_FOR_AGENTS.md +3 -3
- package/README.md +16 -8
- package/dist/audit.js +7 -0
- package/dist/backfill.js +2 -16
- package/dist/cli/fix.d.ts +3 -0
- package/dist/cli/fix.js +70 -1
- package/dist/cli/help.d.ts +6 -0
- package/dist/cli/help.js +76 -2
- package/dist/cli/suggest.d.ts +2 -1
- package/dist/cli/suggest.js +49 -3
- package/dist/cli.js +40 -15
- package/dist/connector.js +96 -25
- package/dist/connectors/hubspot.d.ts +2 -1
- package/dist/connectors/hubspot.js +170 -4
- package/dist/connectors/salesforce.d.ts +2 -1
- package/dist/connectors/salesforce.js +135 -0
- package/dist/connectors/signalSources.js +2 -11
- package/dist/format.js +31 -5
- package/dist/freeEmailDomains.d.ts +1 -0
- package/dist/freeEmailDomains.js +14 -0
- package/dist/hierarchy.d.ts +29 -0
- package/dist/hierarchy.js +164 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +5 -1
- package/dist/mcp.js +19 -15
- package/dist/relationships.d.ts +39 -0
- package/dist/relationships.js +101 -0
- package/dist/route.d.ts +46 -0
- package/dist/route.js +228 -0
- package/dist/rules.d.ts +1 -0
- package/dist/rules.js +172 -12
- package/dist/types.d.ts +30 -0
- package/docs/api.md +22 -4
- package/docs/architecture.md +5 -2
- package/package.json +1 -1
- package/skills/fullstackgtm/SKILL.md +2 -2
- package/src/audit.ts +7 -0
- package/src/backfill.ts +2 -17
- package/src/cli/fix.ts +68 -1
- package/src/cli/help.ts +83 -2
- package/src/cli/suggest.ts +58 -4
- package/src/cli.ts +38 -13
- package/src/connector.ts +94 -20
- package/src/connectors/hubspot.ts +164 -5
- package/src/connectors/salesforce.ts +136 -1
- package/src/connectors/signalSources.ts +2 -12
- package/src/format.ts +33 -5
- package/src/freeEmailDomains.ts +14 -0
- package/src/hierarchy.ts +185 -0
- package/src/index.ts +25 -0
- package/src/mcp.ts +22 -16
- package/src/relationships.ts +115 -0
- package/src/route.ts +278 -0
- package/src/rules.ts +192 -12
- package/src/types.ts +32 -0
package/dist/format.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
// `summary: true` renders
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// where you approve specific operations — keep every operation's detail.
|
|
1
|
+
// `summary: true` renders the compact audit view: header, findings summary,
|
|
2
|
+
// assumptions/open questions, and an operation count. Defaults to the full view
|
|
3
|
+
// so write-preview callers (bulk-update, fix, dedupe, plans show, MCP) — where
|
|
4
|
+
// you approve specific operations — keep every operation's detail.
|
|
6
5
|
export function patchPlanToMarkdown(plan, opts = {}) {
|
|
7
6
|
const lines = [
|
|
8
7
|
`# ${plan.title}`,
|
|
@@ -25,6 +24,7 @@ export function patchPlanToMarkdown(plan, opts = {}) {
|
|
|
25
24
|
});
|
|
26
25
|
lines.push("");
|
|
27
26
|
}
|
|
27
|
+
appendAssumptionsAndDecisionPoints(lines, plan);
|
|
28
28
|
if (opts.summary) {
|
|
29
29
|
const ops = plan.operations.length;
|
|
30
30
|
lines.push(ops === 0
|
|
@@ -96,6 +96,32 @@ export function formatPatchPlanRun(run) {
|
|
|
96
96
|
}
|
|
97
97
|
return `${lines.join("\n")}\n`;
|
|
98
98
|
}
|
|
99
|
+
function appendAssumptionsAndDecisionPoints(lines, plan) {
|
|
100
|
+
const assumptions = [...(plan.assumptions ?? [])].sort((left, right) => {
|
|
101
|
+
if (left.confidence === "guess" && right.confidence !== "guess")
|
|
102
|
+
return -1;
|
|
103
|
+
if (left.confidence !== "guess" && right.confidence === "guess")
|
|
104
|
+
return 1;
|
|
105
|
+
return 0;
|
|
106
|
+
});
|
|
107
|
+
const openQuestions = plan.openQuestions ?? [];
|
|
108
|
+
if (assumptions.length === 0 && openQuestions.length === 0)
|
|
109
|
+
return;
|
|
110
|
+
lines.push("## Assumptions & decision points", "");
|
|
111
|
+
for (const assumption of assumptions) {
|
|
112
|
+
const warning = assumption.confidence === "guess" ? "⚠️ " : "";
|
|
113
|
+
lines.push(`- ${warning}${assumption.text}${assumption.source ? ` _(source: ${assumption.source})_` : ""}`);
|
|
114
|
+
}
|
|
115
|
+
if (openQuestions.length > 0) {
|
|
116
|
+
if (assumptions.length > 0)
|
|
117
|
+
lines.push("");
|
|
118
|
+
lines.push("### Open questions", "");
|
|
119
|
+
for (const question of openQuestions) {
|
|
120
|
+
lines.push(`- ${question}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
lines.push("");
|
|
124
|
+
}
|
|
99
125
|
function formatValue(value) {
|
|
100
126
|
if (value === undefined || value === null || value === "")
|
|
101
127
|
return "unset";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const FREE_EMAIL_DOMAINS: Set<string>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Shared free-mail domains that should never be treated as company domains.
|
|
2
|
+
export const FREE_EMAIL_DOMAINS = new Set([
|
|
3
|
+
"gmail.com",
|
|
4
|
+
"googlemail.com",
|
|
5
|
+
"yahoo.com",
|
|
6
|
+
"outlook.com",
|
|
7
|
+
"hotmail.com",
|
|
8
|
+
"live.com",
|
|
9
|
+
"icloud.com",
|
|
10
|
+
"me.com",
|
|
11
|
+
"aol.com",
|
|
12
|
+
"proton.me",
|
|
13
|
+
"protonmail.com",
|
|
14
|
+
]);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CanonicalGtmSnapshot } from "./types.ts";
|
|
2
|
+
export type AccountHierarchyNode = {
|
|
3
|
+
accountId: string;
|
|
4
|
+
name: string;
|
|
5
|
+
domain?: string;
|
|
6
|
+
parentAccountId?: string;
|
|
7
|
+
parentReason?: string;
|
|
8
|
+
children: AccountHierarchyNode[];
|
|
9
|
+
};
|
|
10
|
+
export type AccountHierarchyConflict = {
|
|
11
|
+
type: "duplicate_domain" | "ambiguous_parent" | "cycle";
|
|
12
|
+
accountIds: string[];
|
|
13
|
+
key: string;
|
|
14
|
+
detail: string;
|
|
15
|
+
};
|
|
16
|
+
export type AccountHierarchyReport = {
|
|
17
|
+
generatedAt: string;
|
|
18
|
+
roots: AccountHierarchyNode[];
|
|
19
|
+
orphanAccounts: string[];
|
|
20
|
+
conflicts: AccountHierarchyConflict[];
|
|
21
|
+
counts: {
|
|
22
|
+
accounts: number;
|
|
23
|
+
roots: number;
|
|
24
|
+
linkedChildren: number;
|
|
25
|
+
conflicts: number;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
export declare function buildAccountHierarchy(snapshot: CanonicalGtmSnapshot): AccountHierarchyReport;
|
|
29
|
+
export declare function accountHierarchyToMarkdown(report: AccountHierarchyReport): string;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { normalizeDomain } from "./merge.js";
|
|
2
|
+
function rawString(account, keys) {
|
|
3
|
+
if (!account.raw || typeof account.raw !== "object")
|
|
4
|
+
return undefined;
|
|
5
|
+
const raw = account.raw;
|
|
6
|
+
for (const key of keys) {
|
|
7
|
+
const value = raw[key];
|
|
8
|
+
if (typeof value === "string" && value.trim())
|
|
9
|
+
return value.trim();
|
|
10
|
+
if (value && typeof value === "object") {
|
|
11
|
+
const nested = value;
|
|
12
|
+
for (const nestedKey of ["id", "value", "name"]) {
|
|
13
|
+
if (typeof nested[nestedKey] === "string" && String(nested[nestedKey]).trim())
|
|
14
|
+
return String(nested[nestedKey]).trim();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
function parentHint(account) {
|
|
21
|
+
return rawString(account, ["parentAccountId", "parent_account_id", "ParentId", "parentCompanyId", "hs_parent_company_id"]);
|
|
22
|
+
}
|
|
23
|
+
function cloneNode(account) {
|
|
24
|
+
return { accountId: String(account.id), name: account.name, ...(account.domain ? { domain: account.domain } : {}), children: [] };
|
|
25
|
+
}
|
|
26
|
+
function looksLikePublicSuffix(domain) {
|
|
27
|
+
const parts = domain.split(".");
|
|
28
|
+
if (parts.length !== 2)
|
|
29
|
+
return false;
|
|
30
|
+
const [label, tld] = parts;
|
|
31
|
+
return tld.length === 2 && ["ac", "co", "com", "edu", "gov", "net", "org"].includes(label);
|
|
32
|
+
}
|
|
33
|
+
function domainParent(childDomain, domains) {
|
|
34
|
+
const parts = childDomain.split(".");
|
|
35
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
36
|
+
const candidate = parts.slice(i).join(".");
|
|
37
|
+
if (candidate.split(".").length < 2 || looksLikePublicSuffix(candidate))
|
|
38
|
+
continue;
|
|
39
|
+
const unique = new Map((domains.get(candidate) ?? []).map((account) => [String(account.id), account]));
|
|
40
|
+
if (unique.size > 1)
|
|
41
|
+
return "ambiguous";
|
|
42
|
+
if (unique.size === 1)
|
|
43
|
+
return [...unique.values()][0];
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function detectCycles(parentByChild) {
|
|
48
|
+
const cyclic = new Set();
|
|
49
|
+
const cycles = [];
|
|
50
|
+
const emitted = new Set();
|
|
51
|
+
for (const start of parentByChild.keys()) {
|
|
52
|
+
const path = [];
|
|
53
|
+
const indexById = new Map();
|
|
54
|
+
let cursor = start;
|
|
55
|
+
while (cursor && parentByChild.has(cursor)) {
|
|
56
|
+
const seenAt = indexById.get(cursor);
|
|
57
|
+
if (seenAt !== undefined) {
|
|
58
|
+
const cycle = path.slice(seenAt);
|
|
59
|
+
for (const id of cycle)
|
|
60
|
+
cyclic.add(id);
|
|
61
|
+
const key = [...cycle].sort().join("|");
|
|
62
|
+
if (!emitted.has(key)) {
|
|
63
|
+
emitted.add(key);
|
|
64
|
+
cycles.push(cycle);
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
indexById.set(cursor, path.length);
|
|
69
|
+
path.push(cursor);
|
|
70
|
+
cursor = parentByChild.get(cursor)?.parentId;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { accountIds: cyclic, cycles };
|
|
74
|
+
}
|
|
75
|
+
export function buildAccountHierarchy(snapshot) {
|
|
76
|
+
const byId = new Map(snapshot.accounts.map((account) => [String(account.id), account]));
|
|
77
|
+
const accounts = [...byId.values()];
|
|
78
|
+
const domains = new Map();
|
|
79
|
+
for (const account of accounts) {
|
|
80
|
+
const domain = normalizeDomain(account.domain);
|
|
81
|
+
if (domain)
|
|
82
|
+
domains.set(domain, [...(domains.get(domain) ?? []), account]);
|
|
83
|
+
}
|
|
84
|
+
const conflicts = [];
|
|
85
|
+
for (const [domain, accounts] of domains) {
|
|
86
|
+
const ids = [...new Set(accounts.map((account) => String(account.id)))];
|
|
87
|
+
if (ids.length > 1)
|
|
88
|
+
conflicts.push({ type: "duplicate_domain", accountIds: ids.sort(), key: domain, detail: `${ids.length} accounts share domain ${domain}; hierarchy inference will not choose between them.` });
|
|
89
|
+
}
|
|
90
|
+
const nodes = new Map(accounts.map((account) => [String(account.id), cloneNode(account)]));
|
|
91
|
+
const parentByChild = new Map();
|
|
92
|
+
for (const account of accounts) {
|
|
93
|
+
const id = String(account.id);
|
|
94
|
+
const hint = parentHint(account);
|
|
95
|
+
if (hint && byId.has(hint) && hint !== id) {
|
|
96
|
+
parentByChild.set(id, { parentId: hint, reason: "provider-parent" });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const domain = normalizeDomain(account.domain);
|
|
100
|
+
if (!domain)
|
|
101
|
+
continue;
|
|
102
|
+
const inferred = domainParent(domain, domains);
|
|
103
|
+
if (inferred === "ambiguous")
|
|
104
|
+
conflicts.push({ type: "ambiguous_parent", accountIds: [id], key: domain, detail: `Multiple possible parent accounts found for ${domain}; leaving account as a root.` });
|
|
105
|
+
else if (inferred && String(inferred.id) !== id)
|
|
106
|
+
parentByChild.set(id, { parentId: String(inferred.id), reason: "domain-subdomain" });
|
|
107
|
+
}
|
|
108
|
+
const cyclicAccounts = detectCycles(parentByChild);
|
|
109
|
+
for (const cycle of cyclicAccounts.cycles) {
|
|
110
|
+
const ordered = [...cycle].sort();
|
|
111
|
+
const key = ordered.join(" -> ");
|
|
112
|
+
conflicts.push({
|
|
113
|
+
type: "cycle",
|
|
114
|
+
accountIds: ordered,
|
|
115
|
+
key,
|
|
116
|
+
detail: `Cyclic parent hints detected among ${ordered.join(", ")}; leaving those accounts as roots instead of hiding them in a loop.`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
for (const accountId of cyclicAccounts.accountIds)
|
|
120
|
+
parentByChild.delete(accountId);
|
|
121
|
+
const roots = [];
|
|
122
|
+
const orphanAccounts = [];
|
|
123
|
+
const linkedChildren = new Set();
|
|
124
|
+
for (const account of accounts) {
|
|
125
|
+
const id = String(account.id);
|
|
126
|
+
const node = nodes.get(id);
|
|
127
|
+
const parent = parentByChild.get(id);
|
|
128
|
+
if (parent && nodes.has(parent.parentId)) {
|
|
129
|
+
node.parentAccountId = parent.parentId;
|
|
130
|
+
node.parentReason = parent.reason;
|
|
131
|
+
nodes.get(parent.parentId).children.push(node);
|
|
132
|
+
linkedChildren.add(id);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
roots.push(node);
|
|
136
|
+
if (!account.domain && !parentHint(account))
|
|
137
|
+
orphanAccounts.push(id);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const sortTree = (list) => {
|
|
141
|
+
list.sort((a, b) => a.name.localeCompare(b.name) || a.accountId.localeCompare(b.accountId));
|
|
142
|
+
for (const node of list)
|
|
143
|
+
sortTree(node.children);
|
|
144
|
+
};
|
|
145
|
+
sortTree(roots);
|
|
146
|
+
return { generatedAt: snapshot.generatedAt, roots, orphanAccounts: orphanAccounts.sort(), conflicts, counts: { accounts: accounts.length, roots: roots.length, linkedChildren: linkedChildren.size, conflicts: conflicts.length } };
|
|
147
|
+
}
|
|
148
|
+
export function accountHierarchyToMarkdown(report) {
|
|
149
|
+
const lines = ["# Account hierarchy", "", `${report.counts.accounts} account(s), ${report.counts.roots} root(s), ${report.counts.linkedChildren} inferred child link(s), ${report.counts.conflicts} conflict(s).`, ""];
|
|
150
|
+
const walk = (nodes, depth) => {
|
|
151
|
+
for (const node of nodes) {
|
|
152
|
+
const suffix = [node.domain, node.parentReason ? `via ${node.parentReason}` : undefined].filter(Boolean).join("; ");
|
|
153
|
+
lines.push(`${" ".repeat(depth)}- ${node.name} (${node.accountId}${suffix ? ` — ${suffix}` : ""})`);
|
|
154
|
+
walk(node.children, depth + 1);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
walk(report.roots, 0);
|
|
158
|
+
if (report.conflicts.length) {
|
|
159
|
+
lines.push("", "## Conflicts");
|
|
160
|
+
for (const conflict of report.conflicts)
|
|
161
|
+
lines.push(`- ${conflict.type} ${conflict.key}: ${conflict.detail}`);
|
|
162
|
+
}
|
|
163
|
+
return lines.join("\n");
|
|
164
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,9 @@ export { matchesTerritory, parseAssignmentPolicy, resolveAssignment, type Assign
|
|
|
3
3
|
export { buildBulkUpdatePlan, isFilterableField, parseWhere, type BulkUpdateOptions } from "./bulkUpdate.ts";
|
|
4
4
|
export { buildDedupePlan, dedupeKey, type DedupeOptions } from "./dedupe.ts";
|
|
5
5
|
export { buildReassignPlans, type ReassignObjectType, type ReassignOptions } from "./reassign.ts";
|
|
6
|
+
export { buildLeadRoutePlan, type LeadRouteCounts, type LeadRouteOptions, type LeadRouteResult, } from "./route.ts";
|
|
7
|
+
export { accountHierarchyToMarkdown, buildAccountHierarchy, type AccountHierarchyConflict, type AccountHierarchyNode, type AccountHierarchyReport, } from "./hierarchy.ts";
|
|
8
|
+
export { buildRelationshipMap, relationshipMapToMarkdown, type RelationshipMap, type StakeholderNode, type StakeholderRole, type StakeholderSentiment, } from "./relationships.ts";
|
|
6
9
|
export { CONFIG_FILE_NAME, loadConfig, mergePolicy, resolveConfiguredRules, type FullstackgtmConfig, type LoadedConfig, } from "./config.ts";
|
|
7
10
|
export { applyPatchPlan, type ApplyPatchPlanOptions } from "./connector.ts";
|
|
8
11
|
export { APPLY_STAGES, BACKFILL_STRIPE_STAGES, composeListeners, createProgressEmitter, CRM_SYNC_STAGES, nullProgressEmitter, SNAPSHOT_PULL_STAGES, STRIPE_SNAPSHOT_STAGES, type ProgressEmitter, type ProgressEvent, type ProgressListener, type ProgressSnapshot, } from "./progress.ts";
|
|
@@ -23,11 +26,12 @@ export { mergeSnapshots, type MergeConflict, type MergeMatch, type MergeReport,
|
|
|
23
26
|
export { createFilePlanStore, type PlanStore, type StoredPlan } from "./planStore.ts";
|
|
24
27
|
export { computeApprovalDigests, loadOrCreateSigningKey, loadSigningKey, signApproval, verifyApprovalDigests, type ApprovalVerification, } from "./integrity.ts";
|
|
25
28
|
export { buildAuditLog, verifyAuditLog, type AuditLogEntry, type AuditLogExport, type AuditLogVerification, } from "./auditLog.ts";
|
|
29
|
+
export { FREE_EMAIL_DOMAINS } from "./freeEmailDomains.ts";
|
|
26
30
|
export { formatPatchPlanRun, patchPlanToMarkdown } from "./format.ts";
|
|
27
31
|
export { computeHealth, summarizeHealth, healthToMarkdown, type HealthEntry, type HealthRollup, type HealthRuleDelta, } from "./health.ts";
|
|
28
32
|
export { auditReportToHtml, auditReportToMarkdown, type ReportOptions } from "./report.ts";
|
|
29
33
|
export { HUBSPOT_DEFAULT_FIELD_MAPPINGS, SALESFORCE_DEFAULT_FIELD_MAPPINGS, mappedField, mappedFields, normalizeFieldMappings, readMappedValue, type CrmObjectType, type FieldMappings, } from "./mappings.ts";
|
|
30
|
-
export { accountSingleSourceRule, activeDealAccountWithoutContactsRule, auditFindingId, buildSnapshotIndex, builtinAuditRules, closingSoonInactiveRule, duplicateAccountDomainRule, duplicateContactEmailRule, duplicateOpenDealRule, missingDealAccountRule, missingDealAmountRule, missingDealOwnerRule, orphanAccountRule, pastCloseDateRule, patchOperationId, provenanceSummary, requiresHumanInput, staleDealRule, } from "./rules.ts";
|
|
34
|
+
export { accountSingleSourceRule, activeDealAccountWithoutContactsRule, auditFindingId, buildSnapshotIndex, builtinAuditRules, closingSoonInactiveRule, isFindingsOnlyRule, duplicateAccountDomainRule, duplicateContactEmailRule, duplicateOpenDealRule, missingDealAccountRule, missingDealAmountRule, missingDealOwnerRule, orphanAccountRule, pastCloseDateRule, patchOperationId, provenanceSummary, requiresHumanInput, staleDealRule, } from "./rules.ts";
|
|
31
35
|
export { extractCallInsights, normalizeTranscript, parseCall, parseTranscript, suggestCallDeal, summarizeInsights, type CallDealSuggestion, type CallInsightType, type ExtractedCallInsight, type ParsedCall, type ParsedTranscriptSegment, } from "./calls.ts";
|
|
32
36
|
export { sampleSnapshot } from "./sampleData.ts";
|
|
33
37
|
export { DEFAULT_MODELS, DEFAULT_RUBRIC, classifyCallLlm, detectProviderFromKey, extractInsightsLlm, forcedToolCall, parseRubric, resolveLlmCredential, scoreCallLlm, validateLlmKey, type CallScorecard, type LlmCallClassification, type LlmCredential, type LlmExtractedInsight, type LlmProvider, type Rubric, type RubricDimension, type ScoreBand, type ScoredDimension, } from "./llm.ts";
|
|
@@ -48,4 +52,4 @@ export { fetchAtsJobs, snippetFor, type AtsBoardSource, type AtsJob, } from "./c
|
|
|
48
52
|
export { scoreBand, judgeRunId, normalizeSpan, isGroundedSpan, scoreAccount, accountRecentlyTouched, bestContactForAccount, deterministicDecision, deterministicWhyNow, JUDGE_SCHEMA, DEFAULT_JUDGE_PROMPT, judgeDecisionLlm, judgeSignals, judgeDir, createFileJudgeStore, type JudgeDecisionKind, type JudgeDecision, type JudgeRun, type JudgeBand, type AccountScore, type JudgeStore, } from "./judge.ts";
|
|
49
53
|
export { draft, authorOpeners, draftOpenerLlm, deterministicOpener, creditedTriggerSignal, draftEvidence, sanitizeOpener, firstLine, firstLineGroundedInTrigger, hasBannedGreeting, isStaleTrigger, draftOperationId, draftEvidenceId, DRAFT_CHANNELS, DRAFT_SCHEMA, DEFAULT_DRAFT_PROMPT, DEFAULT_FRESHNESS_DAYS, type Draft, type RejectedDraft, type DraftResult, type DraftChannel, } from "./draft.ts";
|
|
50
54
|
export { DEFAULT_GOLDEN_SET, DEFAULT_MIN_ACCURACY, HOT_SCORE, defaultJudgeFn, gradeAgainstOutcomes, gradeJudge, parseGoldenSet, type Confusion, type EvalJudgeFn, type GoldenHistory, type GoldenRow, type GradeResult, type OutcomeCalibration, } from "./judgeEval.ts";
|
|
51
|
-
export type { ApprovalStatus, AuditFinding, AuditFindingSeverity, CanonicalAccount, CanonicalActivity, CanonicalContact, CanonicalDeal, CanonicalGtmSnapshot, CanonicalUser, CrmProvider, GtmAuditRule, GtmConnector, GtmEvidence, GtmEvidenceSourceSystem, GtmObjectType, GtmPolicy, GtmRuleContext, GtmRuleResult, GtmSnapshotIndex, PatchOperation, PatchOperationResult, PatchOperationType, PatchPlan, PatchPlanRun, PatchPlanRunStatus, PatchVerification, PipelineFinding, PipelineFindingStatus, PipelineFindingType, ProviderIdentity, RiskLevel, SourceFreshness, } from "./types.ts";
|
|
55
|
+
export type { ApprovalStatus, AuditFinding, AuditFindingSeverity, CanonicalAccount, CanonicalActivity, CanonicalContact, CanonicalDeal, CanonicalGtmSnapshot, CanonicalUser, CrmProvider, GtmAuditRule, GtmConnector, GtmEvidence, GtmEvidenceSourceSystem, GtmObjectType, GtmPolicy, GtmRuleContext, GtmRuleResult, GtmSnapshotIndex, PatchOperation, PatchOperationResult, PatchOperationType, PatchPlan, PatchPlanAssumption, PatchPlanAssumptionConfidence, PatchPlanRun, PatchPlanRunStatus, PatchVerification, PipelineFinding, PipelineFindingStatus, PipelineFindingType, ProviderIdentity, RiskLevel, SourceFreshness, } from "./types.ts";
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,9 @@ export { matchesTerritory, parseAssignmentPolicy, resolveAssignment, } from "./a
|
|
|
3
3
|
export { buildBulkUpdatePlan, isFilterableField, parseWhere } from "./bulkUpdate.js";
|
|
4
4
|
export { buildDedupePlan, dedupeKey } from "./dedupe.js";
|
|
5
5
|
export { buildReassignPlans } from "./reassign.js";
|
|
6
|
+
export { buildLeadRoutePlan, } from "./route.js";
|
|
7
|
+
export { accountHierarchyToMarkdown, buildAccountHierarchy, } from "./hierarchy.js";
|
|
8
|
+
export { buildRelationshipMap, relationshipMapToMarkdown, } from "./relationships.js";
|
|
6
9
|
export { CONFIG_FILE_NAME, loadConfig, mergePolicy, resolveConfiguredRules, } from "./config.js";
|
|
7
10
|
export { applyPatchPlan } from "./connector.js";
|
|
8
11
|
export { APPLY_STAGES, BACKFILL_STRIPE_STAGES, composeListeners, createProgressEmitter, CRM_SYNC_STAGES, nullProgressEmitter, SNAPSHOT_PULL_STAGES, STRIPE_SNAPSHOT_STAGES, } from "./progress.js";
|
|
@@ -23,11 +26,12 @@ export { mergeSnapshots, } from "./merge.js";
|
|
|
23
26
|
export { createFilePlanStore } from "./planStore.js";
|
|
24
27
|
export { computeApprovalDigests, loadOrCreateSigningKey, loadSigningKey, signApproval, verifyApprovalDigests, } from "./integrity.js";
|
|
25
28
|
export { buildAuditLog, verifyAuditLog, } from "./auditLog.js";
|
|
29
|
+
export { FREE_EMAIL_DOMAINS } from "./freeEmailDomains.js";
|
|
26
30
|
export { formatPatchPlanRun, patchPlanToMarkdown } from "./format.js";
|
|
27
31
|
export { computeHealth, summarizeHealth, healthToMarkdown, } from "./health.js";
|
|
28
32
|
export { auditReportToHtml, auditReportToMarkdown } from "./report.js";
|
|
29
33
|
export { HUBSPOT_DEFAULT_FIELD_MAPPINGS, SALESFORCE_DEFAULT_FIELD_MAPPINGS, mappedField, mappedFields, normalizeFieldMappings, readMappedValue, } from "./mappings.js";
|
|
30
|
-
export { accountSingleSourceRule, activeDealAccountWithoutContactsRule, auditFindingId, buildSnapshotIndex, builtinAuditRules, closingSoonInactiveRule, duplicateAccountDomainRule, duplicateContactEmailRule, duplicateOpenDealRule, missingDealAccountRule, missingDealAmountRule, missingDealOwnerRule, orphanAccountRule, pastCloseDateRule, patchOperationId, provenanceSummary, requiresHumanInput, staleDealRule, } from "./rules.js";
|
|
34
|
+
export { accountSingleSourceRule, activeDealAccountWithoutContactsRule, auditFindingId, buildSnapshotIndex, builtinAuditRules, closingSoonInactiveRule, isFindingsOnlyRule, duplicateAccountDomainRule, duplicateContactEmailRule, duplicateOpenDealRule, missingDealAccountRule, missingDealAmountRule, missingDealOwnerRule, orphanAccountRule, pastCloseDateRule, patchOperationId, provenanceSummary, requiresHumanInput, staleDealRule, } from "./rules.js";
|
|
31
35
|
export { extractCallInsights, normalizeTranscript, parseCall, parseTranscript, suggestCallDeal, summarizeInsights, } from "./calls.js";
|
|
32
36
|
export { sampleSnapshot } from "./sampleData.js";
|
|
33
37
|
export { DEFAULT_MODELS, DEFAULT_RUBRIC, classifyCallLlm, detectProviderFromKey, extractInsightsLlm, forcedToolCall, parseRubric, resolveLlmCredential, scoreCallLlm, validateLlmKey, } from "./llm.js";
|
package/dist/mcp.js
CHANGED
|
@@ -129,6 +129,9 @@ function packageVersion() {
|
|
|
129
129
|
return "0.0.0";
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
+
function strictSchema(shape) {
|
|
133
|
+
return z.object(shape).strict();
|
|
134
|
+
}
|
|
132
135
|
// Single registration table. startMcpServer() registers exactly this list,
|
|
133
136
|
// and fullstackgtm_capabilities reports its names from the same array — the
|
|
134
137
|
// advertised inventory cannot drift from what is actually registered.
|
|
@@ -143,7 +146,7 @@ const toolDefinitions = [
|
|
|
143
146
|
description: "Run a dry-run GTM hygiene audit and return a reviewable patch plan. " +
|
|
144
147
|
"Sources: the realistic zero-credential demo CRM (provider: \"demo\" — richest test data), " +
|
|
145
148
|
"the minimal sample dataset, a snapshot file, or a live provider.",
|
|
146
|
-
inputSchema: {
|
|
149
|
+
inputSchema: strictSchema({
|
|
147
150
|
provider: z.enum(["sample", "demo", "hubspot", "salesforce", "stripe"]).optional(),
|
|
148
151
|
inputPath: z.string().optional(),
|
|
149
152
|
configPath: z.string().optional(),
|
|
@@ -151,7 +154,7 @@ const toolDefinitions = [
|
|
|
151
154
|
output: z.enum(["json", "markdown"]).optional(),
|
|
152
155
|
today: z.string().optional(),
|
|
153
156
|
staleDealDays: z.number().int().positive().optional(),
|
|
154
|
-
},
|
|
157
|
+
}),
|
|
155
158
|
},
|
|
156
159
|
handler: async ({ provider, inputPath, configPath, rules, output, today, staleDealDays }) => {
|
|
157
160
|
const loaded = configPath ? loadConfig(configPath) : null;
|
|
@@ -176,11 +179,11 @@ const toolDefinitions = [
|
|
|
176
179
|
description: "Derive values for a plan's requires_human_* placeholder operations from snapshot " +
|
|
177
180
|
"evidence (account-name matching, contact associations), with confidence levels and " +
|
|
178
181
|
"reasons. Read-only; feed accepted values into fullstackgtm_apply's valueOverrides.",
|
|
179
|
-
inputSchema: {
|
|
182
|
+
inputSchema: strictSchema({
|
|
180
183
|
planPath: z.string(),
|
|
181
184
|
provider: z.enum(["sample", "demo", "hubspot", "salesforce", "stripe"]).optional(),
|
|
182
185
|
inputPath: z.string().optional(),
|
|
183
|
-
},
|
|
186
|
+
}),
|
|
184
187
|
},
|
|
185
188
|
handler: async ({ planPath, provider, inputPath }) => {
|
|
186
189
|
const plan = JSON.parse(readFileSync(resolve(process.cwd(), planPath), "utf8"));
|
|
@@ -198,14 +201,14 @@ const toolDefinitions = [
|
|
|
198
201
|
"uses LLM extraction when an Anthropic/OpenAI key is configured in the server " +
|
|
199
202
|
"environment or credential store, else the free deterministic keyword baseline; " +
|
|
200
203
|
"'llm' and 'deterministic' force either. Read-only; every insight is provenance-marked.",
|
|
201
|
-
inputSchema: {
|
|
204
|
+
inputSchema: strictSchema({
|
|
202
205
|
transcript: z.string().optional(),
|
|
203
206
|
transcriptPath: z.string().optional(),
|
|
204
207
|
title: z.string().optional(),
|
|
205
208
|
source: z.enum(["gong", "chorus", "fathom", "manual", "csv", "unknown"]).optional(),
|
|
206
209
|
extractor: z.enum(["auto", "llm", "deterministic"]).optional(),
|
|
207
210
|
model: z.string().optional(),
|
|
208
|
-
},
|
|
211
|
+
}),
|
|
209
212
|
},
|
|
210
213
|
handler: async ({ transcript, transcriptPath, title, source, extractor, model }) => {
|
|
211
214
|
const raw = transcript ??
|
|
@@ -238,7 +241,7 @@ const toolDefinitions = [
|
|
|
238
241
|
"(exists | ambiguous | safe_to_create) with matches and a reason, using the same " +
|
|
239
242
|
"identity keys as the audit/merge engines (account domain, contact email, open-deal " +
|
|
240
243
|
"key). Read-only. Never create on 'exists' or 'ambiguous'.",
|
|
241
|
-
inputSchema: {
|
|
244
|
+
inputSchema: strictSchema({
|
|
242
245
|
objectType: z.enum(["account", "contact", "deal"]),
|
|
243
246
|
name: z.string().optional(),
|
|
244
247
|
domain: z.string().optional(),
|
|
@@ -246,7 +249,7 @@ const toolDefinitions = [
|
|
|
246
249
|
accountId: z.string().optional(),
|
|
247
250
|
provider: z.enum(["sample", "demo", "hubspot", "salesforce", "stripe"]).optional(),
|
|
248
251
|
inputPath: z.string().optional(),
|
|
249
|
-
},
|
|
252
|
+
}),
|
|
250
253
|
},
|
|
251
254
|
handler: async ({ objectType, name, domain, email, accountId, provider, inputPath }) => {
|
|
252
255
|
const snapshot = await readSnapshot(provider, inputPath);
|
|
@@ -259,7 +262,7 @@ const toolDefinitions = [
|
|
|
259
262
|
config: {
|
|
260
263
|
title: "List Audit Rules",
|
|
261
264
|
description: "List the built-in deterministic audit rules with ids and descriptions.",
|
|
262
|
-
inputSchema: {},
|
|
265
|
+
inputSchema: strictSchema({}),
|
|
263
266
|
},
|
|
264
267
|
handler: async () => {
|
|
265
268
|
return content(builtinAuditRules.map(({ id, title, description }) => ({ id, title, description })));
|
|
@@ -277,13 +280,13 @@ const toolDefinitions = [
|
|
|
277
280
|
"the store's HMAC approval digests — ids not approved with `plans approve` are " +
|
|
278
281
|
"refused — and the run is recorded onto the stored plan so `plans show` and " +
|
|
279
282
|
"`audit-log export` include it.",
|
|
280
|
-
inputSchema: {
|
|
283
|
+
inputSchema: strictSchema({
|
|
281
284
|
provider: z.enum(["hubspot", "salesforce"]),
|
|
282
285
|
planPath: z.string(),
|
|
283
286
|
approvedOperationIds: z.array(z.string()).min(1),
|
|
284
287
|
valueOverrides: z.record(z.string(), z.string()).optional(),
|
|
285
288
|
output: z.enum(["json", "markdown"]).optional(),
|
|
286
|
-
},
|
|
289
|
+
}),
|
|
287
290
|
},
|
|
288
291
|
handler: async ({ provider, planPath, approvedOperationIds, valueOverrides, output }) => {
|
|
289
292
|
// The file may be a raw PatchPlan (audit --out) or a StoredPlan envelope
|
|
@@ -400,11 +403,11 @@ const toolDefinitions = [
|
|
|
400
403
|
"and quote verbatim spans (≤300 chars) for every loud/quiet reading. Submit the full " +
|
|
401
404
|
"ObservationSet via fullstackgtm_market_observe — quotes are verified character-for-" +
|
|
402
405
|
"character against the captures, so never paraphrase.",
|
|
403
|
-
inputSchema: {
|
|
406
|
+
inputSchema: strictSchema({
|
|
404
407
|
vendorId: z.string(),
|
|
405
408
|
configPath: z.string().optional().describe("Path to market.config.json (default ./market.config.json)"),
|
|
406
409
|
captureRun: z.string().optional(),
|
|
407
|
-
},
|
|
410
|
+
}),
|
|
408
411
|
},
|
|
409
412
|
handler: async ({ vendorId, configPath, captureRun }) => {
|
|
410
413
|
const config = loadMarketConfigOrHint(resolve(process.cwd(), configPath ?? "market.config.json"));
|
|
@@ -420,10 +423,10 @@ const toolDefinitions = [
|
|
|
420
423
|
"Validates coverage, the verbatim-evidence rule, and mechanically verifies every quoted " +
|
|
421
424
|
"span against the stored capture it cites. Returns problems if rejected; nothing is " +
|
|
422
425
|
"stored unless the whole set passes. Observations are append-only — use a new runLabel.",
|
|
423
|
-
inputSchema: {
|
|
426
|
+
inputSchema: strictSchema({
|
|
424
427
|
observationsPath: z.string().describe("Path to the ObservationSet JSON file"),
|
|
425
428
|
configPath: z.string().optional().describe("Path to market.config.json (default ./market.config.json)"),
|
|
426
|
-
},
|
|
429
|
+
}),
|
|
427
430
|
},
|
|
428
431
|
handler: async ({ observationsPath, configPath }) => {
|
|
429
432
|
const config = loadMarketConfigOrHint(resolve(process.cwd(), configPath ?? "market.config.json"));
|
|
@@ -454,6 +457,7 @@ toolDefinitions.unshift({
|
|
|
454
457
|
description: "Machine-readable contract for this MCP server: the tool inventory (derived from the " +
|
|
455
458
|
"server's own registration table, with per-tool CRM-write flags), safety defaults, and " +
|
|
456
459
|
"the CLI entrypoints for everything the tools don't cover.",
|
|
460
|
+
inputSchema: strictSchema({}),
|
|
457
461
|
},
|
|
458
462
|
handler: async () => content({
|
|
459
463
|
ok: true,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { CanonicalGtmSnapshot } from "./types.ts";
|
|
2
|
+
export type StakeholderRole = "economic_buyer" | "decision_maker" | "technical_buyer" | "champion" | "influencer" | "unknown";
|
|
3
|
+
export type StakeholderSentiment = "positive" | "neutral" | "negative" | "unknown";
|
|
4
|
+
export type StakeholderNode = {
|
|
5
|
+
contactId: string;
|
|
6
|
+
name: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
email?: string;
|
|
9
|
+
role: StakeholderRole;
|
|
10
|
+
sentiment: StakeholderSentiment;
|
|
11
|
+
lastActivityAt?: string;
|
|
12
|
+
evidence: string[];
|
|
13
|
+
};
|
|
14
|
+
export type RelationshipMap = {
|
|
15
|
+
account: {
|
|
16
|
+
accountId: string;
|
|
17
|
+
name: string;
|
|
18
|
+
domain?: string;
|
|
19
|
+
};
|
|
20
|
+
stakeholders: StakeholderNode[];
|
|
21
|
+
openDeals: Array<{
|
|
22
|
+
dealId: string;
|
|
23
|
+
name: string;
|
|
24
|
+
stage?: string;
|
|
25
|
+
amount?: number;
|
|
26
|
+
ownerId?: string;
|
|
27
|
+
}>;
|
|
28
|
+
gaps: string[];
|
|
29
|
+
counts: {
|
|
30
|
+
stakeholders: number;
|
|
31
|
+
openDeals: number;
|
|
32
|
+
activities: number;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
export declare function buildRelationshipMap(snapshot: CanonicalGtmSnapshot, selector: {
|
|
36
|
+
accountId?: string;
|
|
37
|
+
domain?: string;
|
|
38
|
+
}): RelationshipMap;
|
|
39
|
+
export declare function relationshipMapToMarkdown(map: RelationshipMap): string;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { normalizeDomain } from "./merge.js";
|
|
2
|
+
function fullName(contact) {
|
|
3
|
+
return [contact.firstName, contact.lastName].filter(Boolean).join(" ") || contact.email || contact.id;
|
|
4
|
+
}
|
|
5
|
+
function inferRole(title) {
|
|
6
|
+
const t = (title ?? "").toLowerCase();
|
|
7
|
+
if (/\b(cfo|ceo|coo|cro|chief|founder|owner|president)\b/.test(t))
|
|
8
|
+
return "economic_buyer";
|
|
9
|
+
if (/\b(vp|vice president|head of|director|gm|general manager)\b/.test(t))
|
|
10
|
+
return "decision_maker";
|
|
11
|
+
if (/\b(cto|cio|security|it|engineering|architect|developer|technical|data)\b/.test(t))
|
|
12
|
+
return "technical_buyer";
|
|
13
|
+
if (/\b(champion|revops|sales ops|revenue operations|operations)\b/.test(t))
|
|
14
|
+
return "champion";
|
|
15
|
+
if (t)
|
|
16
|
+
return "influencer";
|
|
17
|
+
return "unknown";
|
|
18
|
+
}
|
|
19
|
+
function inferSentiment(text) {
|
|
20
|
+
const lower = text.toLowerCase();
|
|
21
|
+
if (/\b(blocked|concern|risk|no budget|not interested|detractor|negative|unhappy)\b/.test(lower))
|
|
22
|
+
return "negative";
|
|
23
|
+
if (/\b(champion|excited|interested|positive|approved|referred|next step|pilot)\b/.test(lower))
|
|
24
|
+
return "positive";
|
|
25
|
+
return "neutral";
|
|
26
|
+
}
|
|
27
|
+
function maxSentiment(a, b) {
|
|
28
|
+
const rank = { unknown: 0, neutral: 1, positive: 2, negative: 3 };
|
|
29
|
+
return rank[b] > rank[a] ? b : a;
|
|
30
|
+
}
|
|
31
|
+
function findAccount(snapshot, selector) {
|
|
32
|
+
if (selector.accountId)
|
|
33
|
+
return snapshot.accounts.find((account) => String(account.id) === selector.accountId) ?? null;
|
|
34
|
+
if (selector.domain) {
|
|
35
|
+
const domain = normalizeDomain(selector.domain);
|
|
36
|
+
return snapshot.accounts.find((account) => normalizeDomain(account.domain) === domain) ?? null;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
export function buildRelationshipMap(snapshot, selector) {
|
|
41
|
+
const account = findAccount(snapshot, selector);
|
|
42
|
+
if (!account)
|
|
43
|
+
throw new Error("relationships: account not found (use --account-id or --domain)");
|
|
44
|
+
const contacts = snapshot.contacts.filter((contact) => contact.accountId === account.id);
|
|
45
|
+
const contactIds = new Set(contacts.map((contact) => contact.id));
|
|
46
|
+
const activities = snapshot.activities.filter((activity) => activity.accountId === account.id || (activity.contactId ? contactIds.has(activity.contactId) : false));
|
|
47
|
+
const evidenceByContact = new Map();
|
|
48
|
+
const sentimentByContact = new Map();
|
|
49
|
+
for (const activity of activities) {
|
|
50
|
+
if (!activity.contactId || !contactIds.has(activity.contactId))
|
|
51
|
+
continue;
|
|
52
|
+
const text = [activity.subject, activity.type, activity.occurredAt].filter(Boolean).join(" — ");
|
|
53
|
+
evidenceByContact.set(activity.contactId, [...(evidenceByContact.get(activity.contactId) ?? []), text]);
|
|
54
|
+
sentimentByContact.set(activity.contactId, maxSentiment(sentimentByContact.get(activity.contactId) ?? "unknown", inferSentiment(text)));
|
|
55
|
+
}
|
|
56
|
+
const stakeholders = contacts.map((contact) => ({
|
|
57
|
+
contactId: String(contact.id),
|
|
58
|
+
name: fullName(contact),
|
|
59
|
+
...(contact.title ? { title: contact.title } : {}),
|
|
60
|
+
...(contact.email ? { email: contact.email } : {}),
|
|
61
|
+
role: inferRole(contact.title),
|
|
62
|
+
sentiment: sentimentByContact.get(contact.id) ?? "unknown",
|
|
63
|
+
...(contact.lastActivityAt ? { lastActivityAt: contact.lastActivityAt } : {}),
|
|
64
|
+
evidence: evidenceByContact.get(contact.id) ?? [],
|
|
65
|
+
})).sort((a, b) => a.role.localeCompare(b.role) || a.name.localeCompare(b.name));
|
|
66
|
+
const openDeals = snapshot.deals.filter((deal) => deal.accountId === account.id && !deal.isClosed).map((deal) => ({
|
|
67
|
+
dealId: String(deal.id),
|
|
68
|
+
name: deal.name,
|
|
69
|
+
...(deal.stage ? { stage: deal.stage } : {}),
|
|
70
|
+
...(deal.amount !== undefined ? { amount: deal.amount } : {}),
|
|
71
|
+
...(deal.ownerId ? { ownerId: deal.ownerId } : {}),
|
|
72
|
+
}));
|
|
73
|
+
const roles = new Set(stakeholders.map((s) => s.role));
|
|
74
|
+
const gaps = [];
|
|
75
|
+
if (!roles.has("economic_buyer"))
|
|
76
|
+
gaps.push("No obvious economic buyer found from titles.");
|
|
77
|
+
if (!roles.has("decision_maker"))
|
|
78
|
+
gaps.push("No obvious decision maker found from titles.");
|
|
79
|
+
if (stakeholders.every((s) => s.sentiment === "unknown"))
|
|
80
|
+
gaps.push("No stakeholder activity evidence attached to this account.");
|
|
81
|
+
return { account: { accountId: String(account.id), name: account.name, ...(account.domain ? { domain: account.domain } : {}) }, stakeholders, openDeals, gaps, counts: { stakeholders: stakeholders.length, openDeals: openDeals.length, activities: activities.length } };
|
|
82
|
+
}
|
|
83
|
+
export function relationshipMapToMarkdown(map) {
|
|
84
|
+
const lines = [`# Relationship map: ${map.account.name}`, "", `${map.counts.stakeholders} stakeholder(s), ${map.counts.openDeals} open deal(s), ${map.counts.activities} activity record(s).`, "", "## Stakeholders"];
|
|
85
|
+
for (const s of map.stakeholders) {
|
|
86
|
+
lines.push(`- ${s.name} — ${s.role}, ${s.sentiment}${s.title ? ` (${s.title})` : ""}`);
|
|
87
|
+
for (const evidence of s.evidence.slice(0, 3))
|
|
88
|
+
lines.push(` - evidence: ${evidence}`);
|
|
89
|
+
}
|
|
90
|
+
if (map.openDeals.length) {
|
|
91
|
+
lines.push("", "## Open deals");
|
|
92
|
+
for (const deal of map.openDeals)
|
|
93
|
+
lines.push(`- ${deal.name} (${deal.dealId})${deal.stage ? ` — ${deal.stage}` : ""}${deal.amount !== undefined ? ` — ${deal.amount}` : ""}`);
|
|
94
|
+
}
|
|
95
|
+
if (map.gaps.length) {
|
|
96
|
+
lines.push("", "## Gaps");
|
|
97
|
+
for (const gap of map.gaps)
|
|
98
|
+
lines.push(`- ${gap}`);
|
|
99
|
+
}
|
|
100
|
+
return lines.join("\n");
|
|
101
|
+
}
|
package/dist/route.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type AssignmentPolicy } from "./assign.ts";
|
|
2
|
+
import type { CanonicalGtmSnapshot, PatchPlan } from "./types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Lead-to-account matching and routing recipes.
|
|
5
|
+
*
|
|
6
|
+
* This is the first packaged Traction-Complete-style workflow in the OSS CLI:
|
|
7
|
+
* take ownerless/unmatched leads, deterministically match them to accounts, and
|
|
8
|
+
* emit normal approval-gated patch operations. No writes happen here.
|
|
9
|
+
*/
|
|
10
|
+
export type LeadRouteOptions = {
|
|
11
|
+
/** Match contact email domains to account domains. Default true. */
|
|
12
|
+
matchByDomain?: boolean;
|
|
13
|
+
/** Also try raw company/name fields against account names when domain misses. */
|
|
14
|
+
matchByCompanyName?: boolean;
|
|
15
|
+
/** When an ownerless contact matches an owned account, set contact ownerId to the account owner. Default true. */
|
|
16
|
+
inheritAccountOwner?: boolean;
|
|
17
|
+
/** Reassign contacts that already have an owner when the matched account owner differs. Default false. */
|
|
18
|
+
reassignExistingOwner?: boolean;
|
|
19
|
+
/** Optional fallback policy for contacts that still do not have an owner. */
|
|
20
|
+
assignmentPolicy?: AssignmentPolicy;
|
|
21
|
+
/** Refuse to build larger plans unless explicitly raised. Default 500. */
|
|
22
|
+
maxOperations?: number;
|
|
23
|
+
reason?: string;
|
|
24
|
+
};
|
|
25
|
+
export type LeadRouteCounts = {
|
|
26
|
+
contactsScanned: number;
|
|
27
|
+
/** Contacts that ended with an unambiguous account context (existing link, domain match, or company-name match). */
|
|
28
|
+
matchedContacts: number;
|
|
29
|
+
matchedExistingAccount: number;
|
|
30
|
+
matchedByDomain: number;
|
|
31
|
+
matchedByCompanyName: number;
|
|
32
|
+
ambiguousAccountMatches: number;
|
|
33
|
+
skippedFreeEmail: number;
|
|
34
|
+
linkOperations: number;
|
|
35
|
+
/** Owner set_field operations proposed (account-owner inheritance + policy fallback). */
|
|
36
|
+
ownerOperations: number;
|
|
37
|
+
/** Alias for ownerOperations, named for reports that talk about owner-assigned leads. */
|
|
38
|
+
ownerAssigned: number;
|
|
39
|
+
policyAssigned: number;
|
|
40
|
+
unmatched: number;
|
|
41
|
+
};
|
|
42
|
+
export type LeadRouteResult = {
|
|
43
|
+
plan: PatchPlan;
|
|
44
|
+
counts: LeadRouteCounts;
|
|
45
|
+
};
|
|
46
|
+
export declare function buildLeadRoutePlan(snapshot: CanonicalGtmSnapshot, options?: LeadRouteOptions): LeadRouteResult;
|