@sonordev/contracts 2.17.0 → 2.18.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.18.0
4
+
5
+ A minor release: one new entry, `@sonordev/contracts/decision-roles`. Every other entry is unchanged.
6
+
7
+ - **What a job title says about a buying decision.** `DECISION_ROLES` is the four parts a person can play (`owner`, `decision_maker`, `influencer`, `gatekeeper`), strongest first, and `decisionRoleForTitle(title)` returns the one a title names, or `null` when it names none. The role comes from the title alone, by eight rules read in order with the first match deciding, so "Vice President of Sales" is a decision maker, "Executive Assistant to the CEO" is a gatekeeper and "Marketing Coordinator" is an influencer. "Principal" is an owner unless it names a job, so "Principal Engineer" has no role. A title is read whole (it isn't split on "&" or "/") and only its first 200 characters count. `DecisionRole` is the type. Anything that finds the people on a company's website can import it, so two scans of the same page can't give one person two roles.
8
+
3
9
  ## 2.17.0
4
10
 
5
11
  A minor release: one new entry, `@sonordev/contracts/crm-disposition`. Every other entry is unchanged.
package/README.md CHANGED
@@ -41,6 +41,7 @@ pnpm add @sonordev/contracts
41
41
  | `@sonordev/contracts/funnels` | Incomplete drafts, launch definitions, command/readiness/receipt schemas, bounded public pages and presentation-only brand overrides | Sites, APIs, UI and agent adapters share the same rules |
42
42
  | `@sonordev/contracts/inbox` | Which inbox cards are decisions a bulk action leaves alone (`isDecisionCard`, `DECISION_INSIGHT_TYPES`) | Signal's inbox decides what "Mark all read" and "Dismiss all" may touch, the dashboard previews the same result before the write |
43
43
  | `@sonordev/contracts/crm-disposition` | What a rep's call outcome (positive, negative or left voicemail) changes on a prospect: the call's outcome and sentiment, the contact's stage, the company's status, today's reminder and outreach (`dispositionTransitions`), and the reasons a negative outcome carries | the Sonor API applies an outcome, the dashboard's calling screen offers the same three buttons and reasons, the sales extension stops flipping a company on dial |
44
+ | `@sonordev/contracts/decision-roles` | The part a job title says a person plays in a buying decision (`DECISION_ROLES`: owner, decision maker, influencer, gatekeeper), read from the title alone by `decisionRoleForTitle` | the Sonor API's site-people scan and Signal's site-people scout read the same map |
44
45
 
45
46
  ## Focus closing artwork
46
47
 
@@ -149,6 +150,65 @@ dispositionTransitions('negative', { reason: 'do_not_call' })
149
150
  change incompatibly. The wire values (the three outcomes and five reasons) are
150
151
  stored and sent, so they don't change.
151
152
 
153
+ ## Decision roles
154
+
155
+ When a company's website lists its people, what each job title says about a
156
+ buying decision has to be read the same way wherever the list is built.
157
+ `decisionRoleForTitle` is that rule, written once, so a person can't be an
158
+ influencer in one scan and an owner in another. The role comes from the title
159
+ the page gives and never from a model's opinion: a model can find the people,
160
+ and code decides the role.
161
+
162
+ ```ts
163
+ import { DECISION_ROLES, decisionRoleForTitle } from '@sonordev/contracts/decision-roles'
164
+
165
+ DECISION_ROLES // ['owner', 'decision_maker', 'influencer', 'gatekeeper']
166
+ decisionRoleForTitle('Vice President of Sales') // 'decision_maker'
167
+ decisionRoleForTitle('Executive Assistant to the CEO') // 'gatekeeper'
168
+ decisionRoleForTitle('Marketing Coordinator') // 'influencer'
169
+ decisionRoleForTitle('Principal Engineer') // null
170
+ ```
171
+
172
+ The first rule that matches the title decides, in this order:
173
+
174
+ | Order | The title says | Role |
175
+ |---|---|---|
176
+ | 1 | an executive, administrative, office or personal assistant, "assistant to", secretary, receptionist, front desk | `gatekeeper` |
177
+ | 2 | vice president (VP, SVP, EVP, AVP) | `decision_maker` |
178
+ | 3 | owner, co-owner, founder, co-founder, proprietor, president, CEO, chief executive, managing, senior, name or equity partner, a lone "Partner", or "Principal" that isn't a job | `owner` |
179
+ | 4 | a chief of something else (COO, CFO, CTO, CMO, "Chief ... Officer") | `decision_maker` |
180
+ | 5 | general manager (or GM), director, operations manager, estimator, estimating manager, project manager, superintendent | `decision_maker` |
181
+ | 6 | office manager, administrator, administrative, admin | `gatekeeper` |
182
+ | 7 | marketing, HR, human resources, recruiting, talent acquisition, sales, business development | `influencer` |
183
+ | 8 | coordinator | `gatekeeper` |
184
+
185
+ - **Order is the meaning.** "Vice President of Sales" is a decision maker, not
186
+ an owner because it says president and not an influencer because it says
187
+ sales. "Marketing Director" is a decision maker, because director comes
188
+ before marketing. "Marketing Coordinator" is an influencer, because
189
+ marketing comes before coordinator. "Executive Assistant to the CEO" is a
190
+ gatekeeper, because the assistant rule comes before the CEO rule.
191
+ - **"Principal" is an owner unless it names a job.** "Principal" alone is an
192
+ owner. "Principal Engineer", "Principal Architect", "Principal Consultant",
193
+ "Principal Designer", "Principal Developer" and "Principal Analyst" say
194
+ nothing about ownership, so they have no role.
195
+ - **A title is read whole.** A title that names two jobs isn't split, and the
196
+ strongest role doesn't win; the first rule above that matches does. "Owner &
197
+ Office Manager" is an owner. "Receptionist & Owner" is a gatekeeper.
198
+ - **"Assistant" alone isn't a gatekeeper.** "Assistant Project Manager" and
199
+ "Assistant General Manager" are decision makers, as the manager rules read
200
+ them. "Assistant to the Project Manager" is a gatekeeper.
201
+ - **No title or no match is `null`.** "Technician", a bare "Manager",
202
+ "Accounting Manager", an empty string, whitespace, `null`, `undefined` and
203
+ anything that isn't a string all say no role. The map doesn't guess.
204
+ - **Case, `&` and periods don't matter.** "C.E.O.", "FOUNDER & CEO" and
205
+ "Vice-President" read the same as their plain spellings.
206
+ - **Only the start of a title is read.** Titles come off scraped pages, so
207
+ the first 200 characters decide and the rest is ignored.
208
+
209
+ The four role values are stored and sent, so they don't change. `DECISION_ROLES`
210
+ lists them strongest first, which is the order a screen should rank them in.
211
+
152
212
  ## Schema placeholders
153
213
 
154
214
  `schema-placeholders` finds the parts of a JSON-LD value that a template left
@@ -0,0 +1,39 @@
1
+ /**
2
+ * What a job title says about a person's part in a buying decision. Single
3
+ * source of truth for the site-people scan: the Sonor API's deterministic scan
4
+ * (JSON-LD persons) and Signal's site-people scout both read this map, so the
5
+ * same person can't be an influencer on one side and an owner on the other.
6
+ *
7
+ * The role is never a model's opinion. A model finds the people; the title the
8
+ * page gives them decides the role, by the rules below, in this order (first
9
+ * match wins):
10
+ *
11
+ * gatekeeper someone who works for a person, whatever that person's
12
+ * title is: "Executive Assistant to the President",
13
+ * "Receptionist"
14
+ * decision_maker a vice president, matched before the president rule can
15
+ * claim the word
16
+ * owner owner, founder, president, CEO, a managing, senior, name
17
+ * or equity partner, a lone "Partner", and "Principal"
18
+ * unless it names a job ("Principal Engineer")
19
+ * decision_maker a chief of something that isn't the CEO, a general
20
+ * manager (or GM), a director, an operations or project
21
+ * manager, an estimator, a superintendent
22
+ * gatekeeper an office manager, an administrator
23
+ * influencer marketing, HR, recruiting, sales, business development
24
+ * gatekeeper a coordinator, last of all
25
+ *
26
+ * So "Vice President of Sales" is a decision maker (not an owner because it
27
+ * says president, not an influencer because it says sales), "Marketing
28
+ * Director" is a decision maker (director beats marketing) and "Marketing
29
+ * Coordinator" is an influencer. A title that names none of these
30
+ * ("Technician", a bare "Manager") has no role: no guess.
31
+ *
32
+ * Pure data and one small function, no dependencies. The four role values are
33
+ * stored and sent, so they never change.
34
+ */
35
+ /** The four parts a person can play in a buying decision, strongest first. */
36
+ export declare const DECISION_ROLES: readonly ['owner', 'decision_maker', 'influencer', 'gatekeeper'];
37
+ export type DecisionRole = (typeof DECISION_ROLES)[number];
38
+ /** The buying role a job title says, or null when it says none ("Technician", "Manager", no title). */
39
+ export declare function decisionRoleForTitle(title: string | null | undefined): DecisionRole | null;
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ // src/contracts/decision-roles.ts
4
+ var DECISION_ROLES = ["owner", "decision_maker", "influencer", "gatekeeper"];
5
+ var MAX_TITLE_LENGTH = 200;
6
+ var RULES = [
7
+ // Someone who works for a person, whatever that person's title is.
8
+ [/\b(?:executive|administrative|admin|office|personal)\s+assistant\b|\bassistant to\b|\bsecretary\b|\breceptionist\b|\bfront desk\b/, "gatekeeper"],
9
+ // A vice president before the president rule below can claim the word.
10
+ [/\b(?:vice[ -]president|vp|svp|evp|avp)\b/, "decision_maker"],
11
+ [
12
+ /\b(?:owner|co-?owner|founder|co-?founder|proprietor|president|ceo|chief executive(?: officer)?|managing partner|senior partner|name partner|equity partner)\b|\bprincipal\b(?!\s+(?:engineer|architect|consultant|designer|developer|analyst))|^partner$/,
13
+ "owner"
14
+ ],
15
+ // Chief of something else (not the CEO, matched above).
16
+ [/\bchief\b.*\bofficer\b|\b(?:coo|cfo|cto|cmo)\b/, "decision_maker"],
17
+ [/\b(?:general manager|gm|director|operations manager|estimator|estimating manager|project manager|superintendent)\b/, "decision_maker"],
18
+ [/\boffice manager\b|\badministrat(?:or|ive)\b|\badmin\b/, "gatekeeper"],
19
+ [/\b(?:marketing|hr|human resources|recruit(?:er|ing)|talent acquisition|sales|business development)\b/, "influencer"],
20
+ [/\bcoordinator\b/, "gatekeeper"]
21
+ ];
22
+ function decisionRoleForTitle(title) {
23
+ if (typeof title !== "string") return null;
24
+ const t = title.slice(0, MAX_TITLE_LENGTH).toLowerCase().replace(/&/g, " and ").replace(/\./g, "").replace(/\s+/g, " ").trim();
25
+ if (!t) return null;
26
+ for (const [pattern, role] of RULES) if (pattern.test(t)) return role;
27
+ return null;
28
+ }
29
+
30
+ exports.DECISION_ROLES = DECISION_ROLES;
31
+ exports.decisionRoleForTitle = decisionRoleForTitle;
@@ -0,0 +1,28 @@
1
+ // src/contracts/decision-roles.ts
2
+ var DECISION_ROLES = ["owner", "decision_maker", "influencer", "gatekeeper"];
3
+ var MAX_TITLE_LENGTH = 200;
4
+ var RULES = [
5
+ // Someone who works for a person, whatever that person's title is.
6
+ [/\b(?:executive|administrative|admin|office|personal)\s+assistant\b|\bassistant to\b|\bsecretary\b|\breceptionist\b|\bfront desk\b/, "gatekeeper"],
7
+ // A vice president before the president rule below can claim the word.
8
+ [/\b(?:vice[ -]president|vp|svp|evp|avp)\b/, "decision_maker"],
9
+ [
10
+ /\b(?:owner|co-?owner|founder|co-?founder|proprietor|president|ceo|chief executive(?: officer)?|managing partner|senior partner|name partner|equity partner)\b|\bprincipal\b(?!\s+(?:engineer|architect|consultant|designer|developer|analyst))|^partner$/,
11
+ "owner"
12
+ ],
13
+ // Chief of something else (not the CEO, matched above).
14
+ [/\bchief\b.*\bofficer\b|\b(?:coo|cfo|cto|cmo)\b/, "decision_maker"],
15
+ [/\b(?:general manager|gm|director|operations manager|estimator|estimating manager|project manager|superintendent)\b/, "decision_maker"],
16
+ [/\boffice manager\b|\badministrat(?:or|ive)\b|\badmin\b/, "gatekeeper"],
17
+ [/\b(?:marketing|hr|human resources|recruit(?:er|ing)|talent acquisition|sales|business development)\b/, "influencer"],
18
+ [/\bcoordinator\b/, "gatekeeper"]
19
+ ];
20
+ function decisionRoleForTitle(title) {
21
+ if (typeof title !== "string") return null;
22
+ const t = title.slice(0, MAX_TITLE_LENGTH).toLowerCase().replace(/&/g, " and ").replace(/\./g, "").replace(/\s+/g, " ").trim();
23
+ if (!t) return null;
24
+ for (const [pattern, role] of RULES) if (pattern.test(t)) return role;
25
+ return null;
26
+ }
27
+
28
+ export { DECISION_ROLES, decisionRoleForTitle };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sonordev/contracts",
3
- "version": "2.17.0",
4
- "description": "The rules Sonor's sites, APIs and dashboard share: popup blocks and site design tokens, site hosts, SEO page identity, title quality, llms.txt sanitizers, the forms honeypot, fleet heartbeats, slot content and signing, site cache tags, portfolio metrics, the house voice's contraction rule, how a proposal's site plan counts its pages, what separates an error page from a page's content, which stored JSON-LD is a template's unfilled placeholder, where prose splits into sentences, which Sonor records Signal reads into a project's knowledge and the address each one is stored under, what outbound email and text may claim about a business and whether a sent email arrived intact, what a segment of outreach is and who may turn one on, a page's own name taken from its title or its slug, which Broadcast card layouts and review compositions are drawn and what a retired one draws as, what a publication's branding may hold, which literal hosts are refused as public fetch destinations, which inbox cards are decisions a bulk action leaves alone, and what a rep's call outcome (positive, negative or left voicemail) changes on a prospect. Pure functions, no dependencies.",
3
+ "version": "2.18.0",
4
+ "description": "The rules Sonor's sites, APIs and dashboard share: popup blocks and site design tokens, site hosts, SEO page identity, title quality, llms.txt sanitizers, the forms honeypot, fleet heartbeats, slot content and signing, site cache tags, portfolio metrics, the house voice's contraction rule, how a proposal's site plan counts its pages, what separates an error page from a page's content, which stored JSON-LD is a template's unfilled placeholder, where prose splits into sentences, which Sonor records Signal reads into a project's knowledge and the address each one is stored under, what outbound email and text may claim about a business and whether a sent email arrived intact, what a segment of outreach is and who may turn one on, a page's own name taken from its title or its slug, which Broadcast card layouts and review compositions are drawn and what a retired one draws as, what a publication's branding may hold, which literal hosts are refused as public fetch destinations, which inbox cards are decisions a bulk action leaves alone, what a rep's call outcome (positive, negative or left voicemail) changes on a prospect, and which buying role a job title says (owner, decision maker, influencer or gatekeeper). Pure functions, no dependencies.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -160,6 +160,11 @@
160
160
  "types": "./dist/crm-disposition.d.ts",
161
161
  "import": "./dist/crm-disposition.mjs",
162
162
  "require": "./dist/crm-disposition.js"
163
+ },
164
+ "./decision-roles": {
165
+ "types": "./dist/decision-roles.d.ts",
166
+ "import": "./dist/decision-roles.mjs",
167
+ "require": "./dist/decision-roles.js"
163
168
  }
164
169
  },
165
170
  "typesVersions": {