fullstackgtm 0.34.0 → 0.38.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +141 -0
  2. package/INSTALL_FOR_AGENTS.md +8 -0
  3. package/README.md +39 -0
  4. package/dist/acquireLinkedIn.d.ts +41 -0
  5. package/dist/acquireLinkedIn.js +57 -0
  6. package/dist/acquireMeter.d.ts +67 -0
  7. package/dist/acquireMeter.js +145 -0
  8. package/dist/acquireSeen.d.ts +5 -0
  9. package/dist/acquireSeen.js +54 -0
  10. package/dist/assign.d.ts +83 -0
  11. package/dist/assign.js +146 -0
  12. package/dist/bin.js +14 -2
  13. package/dist/cli.js +817 -26
  14. package/dist/connectors/hubspot.js +140 -0
  15. package/dist/connectors/linkedin.d.ts +78 -0
  16. package/dist/connectors/linkedin.js +199 -0
  17. package/dist/connectors/prospectSources.d.ts +91 -0
  18. package/dist/connectors/prospectSources.js +227 -0
  19. package/dist/enrich.d.ts +107 -0
  20. package/dist/enrich.js +315 -5
  21. package/dist/format.d.ts +3 -1
  22. package/dist/format.js +14 -2
  23. package/dist/health.d.ts +71 -0
  24. package/dist/health.js +172 -0
  25. package/dist/icp.d.ts +96 -0
  26. package/dist/icp.js +256 -0
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +2 -0
  29. package/dist/mappings.js +3 -0
  30. package/dist/reassign.d.ts +11 -2
  31. package/dist/reassign.js +13 -6
  32. package/dist/runReport.d.ts +9 -0
  33. package/dist/runReport.js +66 -0
  34. package/dist/types.d.ts +25 -1
  35. package/docs/api.md +53 -3
  36. package/docs/architecture.md +11 -1
  37. package/docs/dx-punch-list.md +87 -0
  38. package/docs/linkedin-connector-spec.md +87 -0
  39. package/llms.txt +38 -1
  40. package/package.json +1 -1
  41. package/skills/fullstackgtm/SKILL.md +5 -3
  42. package/src/acquireLinkedIn.ts +83 -0
  43. package/src/acquireMeter.ts +186 -0
  44. package/src/acquireSeen.ts +57 -0
  45. package/src/assign.ts +193 -0
  46. package/src/bin.ts +17 -4
  47. package/src/cli.ts +965 -25
  48. package/src/connectors/hubspot.ts +145 -0
  49. package/src/connectors/linkedin.ts +272 -0
  50. package/src/connectors/prospectSources.ts +324 -0
  51. package/src/enrich.ts +411 -5
  52. package/src/format.ts +20 -2
  53. package/src/health.ts +238 -0
  54. package/src/icp.ts +313 -0
  55. package/src/index.ts +18 -0
  56. package/src/mappings.ts +3 -0
  57. package/src/reassign.ts +24 -8
  58. package/src/runReport.ts +76 -0
  59. package/src/types.ts +32 -0
@@ -0,0 +1,83 @@
1
+ /**
2
+ * AssignmentPolicy — the shared rule that decides which owner a record belongs
3
+ * to. One policy, two consumers:
4
+ * 1. `enrich acquire` stamps the resolved owner into each `create_record`
5
+ * payload, so a lead is never born ownerless, and
6
+ * 2. `reassign --assign-unowned` applies the SAME policy to backfill existing
7
+ * ownerless records.
8
+ *
9
+ * Pure + zero-dep: `resolveAssignment` takes plain data (the record's routing
10
+ * attributes + the candidate owners + the within-run index) and returns an
11
+ * ownerId or null. Round-robin distributes by the within-run index, so it needs
12
+ * no persisted rotation cursor — the same plan always resolves the same way
13
+ * (deterministic apply). Every result is gated by the set of KNOWN active
14
+ * owners: a policy that points at an unknown or inactive owner yields null, and
15
+ * the caller leaves that record unassigned + surfaces it — never a bad write.
16
+ */
17
+ export type AssignmentStrategy = "fixed" | "round-robin" | "territory" | "account-owner";
18
+ /** The attributes a territory rule can route on, lifted from a record/prospect. */
19
+ export type AssignmentContext = {
20
+ /** ISO country code, lowercased (e.g. "us"). */
21
+ geo?: string;
22
+ /** human industry label, lowercased (e.g. "software"). */
23
+ industry?: string;
24
+ /** provider-agnostic employee band (e.g. "11-50"). */
25
+ employeeBand?: string;
26
+ /** department, lowercased (e.g. "sales"). */
27
+ department?: string;
28
+ /** raw job title, lowercased — matched against rule titleKeywords as substrings. */
29
+ title?: string;
30
+ /** owner of the associated account, if any — enables "account-owner" inherit. */
31
+ accountOwnerId?: string;
32
+ };
33
+ /**
34
+ * One territory clause. Every present key must match (AND across keys); within a
35
+ * key the candidate value must be one of the listed values (OR). `titleKeywords`
36
+ * matches when the context title CONTAINS any keyword as a substring.
37
+ */
38
+ export type TerritoryRule = {
39
+ when: Partial<{
40
+ geo: string[];
41
+ industry: string[];
42
+ employeeBand: string[];
43
+ department: string[];
44
+ titleKeywords: string[];
45
+ }>;
46
+ ownerId: string;
47
+ };
48
+ export type AssignmentPolicy = {
49
+ strategy: "fixed";
50
+ ownerId: string;
51
+ } | {
52
+ strategy: "round-robin";
53
+ ownerIds: string[];
54
+ } | {
55
+ strategy: "territory";
56
+ rules: TerritoryRule[];
57
+ fallbackOwnerId?: string;
58
+ } | {
59
+ strategy: "account-owner";
60
+ fallbackOwnerId?: string;
61
+ };
62
+ export type AssignmentResult = {
63
+ /** the resolved owner, or null when the policy can't place this record. */
64
+ ownerId: string | null;
65
+ /** short human label for why (audit trail): "fixed", "round-robin", "territory:0", "account-owner", "fallback". */
66
+ rule?: string;
67
+ };
68
+ /**
69
+ * Validate + normalize a raw assignment policy (from enrich.config.json or a
70
+ * CLI flag). Throws on shape errors so a misconfigured policy fails loud at
71
+ * plan time, never silently mis-routes.
72
+ */
73
+ export declare function parseAssignmentPolicy(raw: unknown): AssignmentPolicy;
74
+ /** Does the context satisfy every present clause of a territory rule? */
75
+ export declare function matchesTerritory(rule: TerritoryRule, ctx: AssignmentContext): boolean;
76
+ /**
77
+ * Resolve the owner for one record. `index` is the record's 0-based position
78
+ * within the run (used by round-robin for deterministic, even within-run
79
+ * distribution without persisted state). `knownOwnerIds` is the set of active
80
+ * owners from the snapshot; any result outside it collapses to null so callers
81
+ * never write an owner the CRM won't accept.
82
+ */
83
+ export declare function resolveAssignment(policy: AssignmentPolicy, ctx: AssignmentContext, index: number, knownOwnerIds: ReadonlySet<string>): AssignmentResult;
package/dist/assign.js ADDED
@@ -0,0 +1,146 @@
1
+ /**
2
+ * AssignmentPolicy — the shared rule that decides which owner a record belongs
3
+ * to. One policy, two consumers:
4
+ * 1. `enrich acquire` stamps the resolved owner into each `create_record`
5
+ * payload, so a lead is never born ownerless, and
6
+ * 2. `reassign --assign-unowned` applies the SAME policy to backfill existing
7
+ * ownerless records.
8
+ *
9
+ * Pure + zero-dep: `resolveAssignment` takes plain data (the record's routing
10
+ * attributes + the candidate owners + the within-run index) and returns an
11
+ * ownerId or null. Round-robin distributes by the within-run index, so it needs
12
+ * no persisted rotation cursor — the same plan always resolves the same way
13
+ * (deterministic apply). Every result is gated by the set of KNOWN active
14
+ * owners: a policy that points at an unknown or inactive owner yields null, and
15
+ * the caller leaves that record unassigned + surfaces it — never a bad write.
16
+ */
17
+ const STRATEGIES = ["fixed", "round-robin", "territory", "account-owner"];
18
+ function asStringArray(value, label) {
19
+ if (!Array.isArray(value) || value.length === 0 || value.some((v) => typeof v !== "string" || !v.trim())) {
20
+ throw new Error(`assign: ${label} must be a non-empty array of strings`);
21
+ }
22
+ return value.map((v) => v.trim());
23
+ }
24
+ /**
25
+ * Validate + normalize a raw assignment policy (from enrich.config.json or a
26
+ * CLI flag). Throws on shape errors so a misconfigured policy fails loud at
27
+ * plan time, never silently mis-routes.
28
+ */
29
+ export function parseAssignmentPolicy(raw) {
30
+ if (!raw || typeof raw !== "object")
31
+ throw new Error("assign: expected a policy object");
32
+ const p = raw;
33
+ const strategy = p.strategy;
34
+ if (typeof strategy !== "string" || !STRATEGIES.includes(strategy)) {
35
+ throw new Error(`assign: "strategy" must be one of ${STRATEGIES.join(" | ")}`);
36
+ }
37
+ switch (strategy) {
38
+ case "fixed": {
39
+ if (typeof p.ownerId !== "string" || !p.ownerId.trim()) {
40
+ throw new Error('assign: fixed strategy needs a non-empty "ownerId"');
41
+ }
42
+ return { strategy, ownerId: p.ownerId.trim() };
43
+ }
44
+ case "round-robin": {
45
+ const ownerIds = asStringArray(p.ownerIds, "round-robin ownerIds");
46
+ return { strategy, ownerIds };
47
+ }
48
+ case "territory": {
49
+ if (!Array.isArray(p.rules) || p.rules.length === 0) {
50
+ throw new Error('assign: territory strategy needs a non-empty "rules" array');
51
+ }
52
+ const rules = p.rules.map((r, i) => {
53
+ if (!r || typeof r !== "object")
54
+ throw new Error(`assign: territory rule ${i} must be an object`);
55
+ const rule = r;
56
+ if (typeof rule.ownerId !== "string" || !rule.ownerId.trim()) {
57
+ throw new Error(`assign: territory rule ${i} needs a non-empty "ownerId"`);
58
+ }
59
+ const whenRaw = (rule.when ?? {});
60
+ const when = {};
61
+ for (const key of ["geo", "industry", "employeeBand", "department", "titleKeywords"]) {
62
+ if (whenRaw[key] !== undefined)
63
+ when[key] = asStringArray(whenRaw[key], `territory rule ${i} when.${key}`);
64
+ }
65
+ if (Object.keys(when).length === 0) {
66
+ throw new Error(`assign: territory rule ${i} "when" needs at least one clause (else it matches everything — use a fallbackOwnerId instead)`);
67
+ }
68
+ return { when, ownerId: rule.ownerId.trim() };
69
+ });
70
+ const fallbackOwnerId = typeof p.fallbackOwnerId === "string" && p.fallbackOwnerId.trim() ? p.fallbackOwnerId.trim() : undefined;
71
+ return { strategy, rules, ...(fallbackOwnerId ? { fallbackOwnerId } : {}) };
72
+ }
73
+ case "account-owner": {
74
+ const fallbackOwnerId = typeof p.fallbackOwnerId === "string" && p.fallbackOwnerId.trim() ? p.fallbackOwnerId.trim() : undefined;
75
+ return { strategy, ...(fallbackOwnerId ? { fallbackOwnerId } : {}) };
76
+ }
77
+ default:
78
+ throw new Error(`assign: unsupported strategy "${strategy}"`);
79
+ }
80
+ }
81
+ function clauseMatches(values, candidate) {
82
+ if (candidate === undefined)
83
+ return false;
84
+ return values.some((v) => v.toLowerCase() === candidate.toLowerCase());
85
+ }
86
+ function titleMatches(keywords, title) {
87
+ if (!title)
88
+ return false;
89
+ const lower = title.toLowerCase();
90
+ return keywords.some((k) => lower.includes(k.toLowerCase()));
91
+ }
92
+ /** Does the context satisfy every present clause of a territory rule? */
93
+ export function matchesTerritory(rule, ctx) {
94
+ const { when } = rule;
95
+ if (when.geo && !clauseMatches(when.geo, ctx.geo))
96
+ return false;
97
+ if (when.industry && !clauseMatches(when.industry, ctx.industry))
98
+ return false;
99
+ if (when.employeeBand && !clauseMatches(when.employeeBand, ctx.employeeBand))
100
+ return false;
101
+ if (when.department && !clauseMatches(when.department, ctx.department))
102
+ return false;
103
+ if (when.titleKeywords && !titleMatches(when.titleKeywords, ctx.title))
104
+ return false;
105
+ return true;
106
+ }
107
+ /** Gate a candidate ownerId by the known-active set: returns it, or null. */
108
+ function known(ownerId, knownOwnerIds) {
109
+ return ownerId && knownOwnerIds.has(ownerId) ? ownerId : null;
110
+ }
111
+ /**
112
+ * Resolve the owner for one record. `index` is the record's 0-based position
113
+ * within the run (used by round-robin for deterministic, even within-run
114
+ * distribution without persisted state). `knownOwnerIds` is the set of active
115
+ * owners from the snapshot; any result outside it collapses to null so callers
116
+ * never write an owner the CRM won't accept.
117
+ */
118
+ export function resolveAssignment(policy, ctx, index, knownOwnerIds) {
119
+ switch (policy.strategy) {
120
+ case "fixed":
121
+ return { ownerId: known(policy.ownerId, knownOwnerIds), rule: "fixed" };
122
+ case "round-robin": {
123
+ const pool = policy.ownerIds.filter((id) => knownOwnerIds.has(id));
124
+ if (pool.length === 0)
125
+ return { ownerId: null, rule: "round-robin" };
126
+ const safeIndex = ((index % pool.length) + pool.length) % pool.length;
127
+ return { ownerId: pool[safeIndex], rule: "round-robin" };
128
+ }
129
+ case "territory": {
130
+ for (let i = 0; i < policy.rules.length; i++) {
131
+ if (matchesTerritory(policy.rules[i], ctx)) {
132
+ const hit = known(policy.rules[i].ownerId, knownOwnerIds);
133
+ if (hit)
134
+ return { ownerId: hit, rule: `territory:${i}` };
135
+ }
136
+ }
137
+ return { ownerId: known(policy.fallbackOwnerId, knownOwnerIds), rule: "fallback" };
138
+ }
139
+ case "account-owner": {
140
+ const inherited = known(ctx.accountOwnerId, knownOwnerIds);
141
+ if (inherited)
142
+ return { ownerId: inherited, rule: "account-owner" };
143
+ return { ownerId: known(policy.fallbackOwnerId, knownOwnerIds), rule: "fallback" };
144
+ }
145
+ }
146
+ }
package/dist/bin.js CHANGED
@@ -1,6 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { runCli } from "./cli.js";
3
- runCli(process.argv.slice(2)).catch((error) => {
4
- console.error(error instanceof Error ? error.message : String(error));
3
+ import { flushRunReport } from "./runReport.js";
4
+ const args = process.argv.slice(2);
5
+ const startedAt = Date.now();
6
+ runCli(args)
7
+ .then(async () => {
8
+ // exitCode 2 (audit findings / create-gate) or 1 (no value) = "partial":
9
+ // the command ran but flagged something. No exitCode = clean success.
10
+ const status = process.exitCode && process.exitCode !== 0 ? "partial" : "success";
11
+ await flushRunReport(args, status, startedAt);
12
+ })
13
+ .catch(async (error) => {
14
+ const message = error instanceof Error ? error.message : String(error);
15
+ await flushRunReport(args, "error", startedAt, message);
16
+ console.error(message);
5
17
  process.exitCode = 1;
6
18
  });