@sonordev/contracts 2.7.1 → 2.8.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.
@@ -0,0 +1,328 @@
1
+ // src/contracts/business-claims.ts
2
+ function fnv1a(text) {
3
+ let hash = 2166136261;
4
+ for (let i = 0; i < text.length; i++) {
5
+ hash ^= text.charCodeAt(i);
6
+ hash = Math.imul(hash, 16777619);
7
+ }
8
+ return (hash >>> 0).toString(16).padStart(8, "0");
9
+ }
10
+
11
+ // src/contracts/outreach.ts
12
+ var OUTREACH_CONTRACT_VERSION = 1;
13
+ var OUTREACH_WRITER_BADGE = "signal.outreach.writer";
14
+ var OUTREACH_EMAIL_KIND = "outreach_email";
15
+ var GRADUATION_BAR = { minMeasured: 5, minWinRate: 0.7, maxUnrevertedDegradations: 0 };
16
+ var SEGMENT_STATES = ["ungranted", "granted", "paused", "lapsed", "revoked"];
17
+ var UNSPECIFIED_INDUSTRY = "unspecified";
18
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
19
+ var isUuid = (value) => typeof value === "string" && UUID.test(value);
20
+ var isRecord = (value) => !!value && typeof value === "object" && !Array.isArray(value);
21
+ var nonEmpty = (value) => typeof value === "string" && value.trim().length > 0;
22
+ function prospectIndustry(prospect) {
23
+ for (const value of [prospect?.vertical, prospect?.industry, prospect?.company_industry]) {
24
+ if (nonEmpty(value)) return value.trim();
25
+ }
26
+ return null;
27
+ }
28
+ function normalizeIndustry(value) {
29
+ if (typeof value !== "string") return UNSPECIFIED_INDUSTRY;
30
+ const slug = value.normalize("NFKC").normalize("NFD").replace(/\p{M}/gu, "").toLowerCase().replace(/&/g, " and ").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48).replace(/-+$/g, "");
31
+ return slug || UNSPECIFIED_INDUSTRY;
32
+ }
33
+ var NARRATIVE_PITCH_FIELDS = [
34
+ "positioning_statement",
35
+ "target_icp_description",
36
+ "tone_profile",
37
+ "hook_types",
38
+ "example_angles",
39
+ "value_prop_bullets"
40
+ ];
41
+ function canonical(value) {
42
+ if (value === null || value === void 0) return "null";
43
+ if (typeof value === "string") return JSON.stringify(value.normalize("NFKC").replace(/\s+/g, " ").trim());
44
+ if (typeof value === "number" || typeof value === "boolean") return JSON.stringify(value);
45
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
46
+ if (isRecord(value)) {
47
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
48
+ }
49
+ return "null";
50
+ }
51
+ function narrativeFingerprint(narrative) {
52
+ const source = isRecord(narrative) ? narrative : {};
53
+ const pitch = {};
54
+ for (const field of NARRATIVE_PITCH_FIELDS) pitch[field] = source[field] ?? null;
55
+ return fnv1a(canonical(pitch));
56
+ }
57
+ function outreachSegment(input) {
58
+ const vertical = normalizeIndustry(input?.industry);
59
+ const label = typeof input?.label === "string" ? input.label : "";
60
+ const angle_fp = typeof input?.angleFp === "string" && input.angleFp ? input.angleFp : null;
61
+ if (input?.source === "drip") {
62
+ if (!isUuid(input.narrativeId) || !isUuid(input.mailboxId)) return null;
63
+ const narrative_id = input.narrativeId.toLowerCase();
64
+ const mailbox_id = input.mailboxId.toLowerCase();
65
+ return { key: `drip:${narrative_id}@${mailbox_id}|${vertical}`, source: "drip", narrative_id, mailbox_id, vertical, label, angle_fp };
66
+ }
67
+ if (input?.source === "mcp") {
68
+ const mailbox_id = isUuid(input.mailboxId) ? input.mailboxId.toLowerCase() : null;
69
+ return { key: `mcp:-|${vertical}`, source: "mcp", narrative_id: null, mailbox_id, vertical, label, angle_fp };
70
+ }
71
+ return null;
72
+ }
73
+ function outreachSegmentLabel(input) {
74
+ const name = nonEmpty(input?.narrativeName) ? input.narrativeName.trim() : "Outreach";
75
+ const industry = nonEmpty(input?.industry) ? input.industry.trim() : "leads with no industry";
76
+ const from = nonEmpty(input?.mailboxAddress) ? `, from ${input.mailboxAddress.trim()}` : "";
77
+ return `${name} to ${industry}${from}`;
78
+ }
79
+ var DRIP_KEY = /^drip:([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})@([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\|([a-z0-9-]{1,48})$/;
80
+ var MCP_KEY = /^mcp:-\|([a-z0-9-]{1,48})$/;
81
+ function parseOutreachSegmentKey(key) {
82
+ if (typeof key !== "string" || key.length > 140) return null;
83
+ const drip = DRIP_KEY.exec(key);
84
+ if (drip) return { source: "drip", narrative_id: drip[1], mailbox_id: drip[2], vertical: drip[3] };
85
+ const mcp = MCP_KEY.exec(key);
86
+ if (mcp) return { source: "mcp", vertical: mcp[1] };
87
+ return null;
88
+ }
89
+ function isGrantableSegmentKey(key) {
90
+ const parsed = parseOutreachSegmentKey(key);
91
+ return parsed?.source === "drip" && parsed.vertical !== UNSPECIFIED_INDUSTRY;
92
+ }
93
+ var isDate = (value) => typeof value === "string" && !Number.isNaN(Date.parse(value));
94
+ function readGrant(value) {
95
+ if (!isRecord(value)) return null;
96
+ const { state, source, by, via, at, ref, receipts, mailbox_id, label, angle_fp, paused, resumed, revoked } = value;
97
+ if (state !== "granted" && state !== "revoked") return null;
98
+ if (source !== "first_email" && source !== "earned") return null;
99
+ if (via !== "dashboard" && via !== "drafts") return null;
100
+ if (!nonEmpty(by) || !isDate(at) || !nonEmpty(ref) || !nonEmpty(mailbox_id) || !nonEmpty(angle_fp) || typeof label !== "string") return null;
101
+ const grant = { state, source, by, via, at, ref, mailbox_id, label, angle_fp };
102
+ if (receipts !== void 0) {
103
+ if (!Array.isArray(receipts) || !receipts.every(nonEmpty)) return null;
104
+ grant.receipts = [...receipts];
105
+ }
106
+ if (paused !== void 0 && paused !== null) {
107
+ if (!isRecord(paused) || !isDate(paused.at) || typeof paused.reason !== "string") return null;
108
+ grant.paused = { at: paused.at, reason: paused.reason, loss_ref: nonEmpty(paused.loss_ref) ? paused.loss_ref : null };
109
+ }
110
+ if (resumed !== void 0 && resumed !== null) {
111
+ if (!isRecord(resumed) || !nonEmpty(resumed.by) || !isDate(resumed.at)) return null;
112
+ grant.resumed = { by: resumed.by, at: resumed.at };
113
+ }
114
+ if (revoked !== void 0 && revoked !== null) {
115
+ if (!isRecord(revoked) || !nonEmpty(revoked.by) || !isDate(revoked.at) || typeof revoked.reason !== "string") return null;
116
+ grant.revoked = { by: revoked.by, at: revoked.at, reason: revoked.reason };
117
+ }
118
+ return grant;
119
+ }
120
+ function readSegmentGrants(source) {
121
+ const autonomy = isRecord(source) && isRecord(source.autonomy) ? source.autonomy : source;
122
+ const segments = isRecord(autonomy) && isRecord(autonomy.segments) ? autonomy.segments[OUTREACH_EMAIL_KIND] : null;
123
+ const out = {};
124
+ if (!isRecord(segments)) return out;
125
+ for (const key of Object.keys(segments)) {
126
+ if (!parseOutreachSegmentKey(key)) continue;
127
+ const grant = readGrant(segments[key]);
128
+ if (grant) out[key] = grant;
129
+ }
130
+ return out;
131
+ }
132
+ function activeSegmentGrant(source, key) {
133
+ if (!isGrantableSegmentKey(key)) return null;
134
+ const grant = readSegmentGrants(source)[key];
135
+ return grant && grant.state === "granted" && !grant.paused ? grant : null;
136
+ }
137
+ function segmentGrantCovers(grant, row) {
138
+ if (!grant) return { covers: false, code: "not_granted", reason: "Nobody's turned this segment on." };
139
+ if (grant.state === "revoked") return { covers: false, code: "revoked", reason: "Someone stopped this segment." };
140
+ if (grant.paused) return { covers: false, code: "paused", reason: `This segment stopped itself: ${grant.paused.reason}` };
141
+ if (grant.state !== "granted") return { covers: false, code: "not_granted", reason: "Nobody's turned this segment on." };
142
+ if (row.by !== void 0 && row.by !== grant.by) return { covers: false, code: "other_person", reason: "This row names someone other than the person who turned the segment on." };
143
+ const created = Date.parse(row.createdAt ?? "");
144
+ const granted = Date.parse(grant.at);
145
+ if (!Number.isFinite(created) || !Number.isFinite(granted) || created < granted) {
146
+ return { covers: false, code: "granted_after", reason: "This email was written before the segment was turned on." };
147
+ }
148
+ if (!row.angleFp || row.angleFp !== grant.angle_fp) {
149
+ return { covers: false, code: "angle_changed", reason: "The pitch changed since this segment was turned on, so its next email needs a look." };
150
+ }
151
+ if (!row.mailboxId || row.mailboxId.toLowerCase() !== grant.mailbox_id.toLowerCase()) {
152
+ return { covers: false, code: "other_mailbox", reason: "This email sends from a different mailbox than the one the segment was turned on for." };
153
+ }
154
+ return { covers: true };
155
+ }
156
+ function segmentOf(row) {
157
+ return isRecord(row.target_ref) && isRecord(row.target_ref.segment) ? row.target_ref.segment : null;
158
+ }
159
+ function payloadOf(row) {
160
+ return isRecord(row.payload) ? row.payload : {};
161
+ }
162
+ function viaOf(row) {
163
+ const approval = payloadOf(row)._approval;
164
+ return isRecord(approval) ? approval.via : void 0;
165
+ }
166
+ function wasEdited(row) {
167
+ const edited = payloadOf(row).edited;
168
+ return isRecord(edited) && typeof edited.count === "number" && edited.count > 0;
169
+ }
170
+ function readBackStatus(row) {
171
+ const execution = payloadOf(row)._execution;
172
+ return isRecord(execution) && isRecord(execution.read_back) ? execution.read_back.status : void 0;
173
+ }
174
+ function evidenceRowRefusal(row, grant, scope, people) {
175
+ if (!row) return "isn't on record";
176
+ if (row.kind !== OUTREACH_EMAIL_KIND || row.created_by !== OUTREACH_WRITER_BADGE) return "isn't an email Signal's outreach writer wrote";
177
+ if (scope.projectId && row.project_id && row.project_id !== scope.projectId) return "belongs to another project";
178
+ if (row.status !== "applied") return "wasn't approved and sent";
179
+ const segment = segmentOf(row);
180
+ if (!segment || segment.key !== scope.key) return "is in a different segment";
181
+ if (typeof segment.mailbox_id !== "string" || segment.mailbox_id.toLowerCase() !== grant.mailbox_id.toLowerCase()) return "went out from a different mailbox";
182
+ if (typeof segment.angle_fp === "string" && segment.angle_fp !== grant.angle_fp) return "was written for an earlier version of the pitch";
183
+ if (!nonEmpty(row.reviewed_by) || row.reviewed_by.startsWith("agent:")) return "wasn't approved by a person";
184
+ if (people && !people.has(row.reviewed_by)) return "wasn't approved by a person";
185
+ const reviewed = Date.parse(row.reviewed_at ?? "");
186
+ if (!Number.isFinite(reviewed) || reviewed > Date.parse(grant.at)) return "was approved after the segment was turned on";
187
+ const via = viaOf(row);
188
+ if (via !== "dashboard" && via !== "drafts") return via === "mcp" ? "was approved through MCP, not in Sonor" : "doesn't say where it was approved";
189
+ if (wasEdited(row)) return "was edited before it was approved";
190
+ if (readBackStatus(row) !== "verified") return "wasn't confirmed to have arrived intact";
191
+ return null;
192
+ }
193
+ function grantEvidenceRefusal(grant, refRows, scope) {
194
+ if (!grant) return "There's no grant.";
195
+ if (!isGrantableSegmentKey(scope?.key)) return "This segment can't be turned on.";
196
+ const rows = /* @__PURE__ */ new Map();
197
+ for (const row of refRows ?? []) if (row && typeof row.id === "string") rows.set(row.id, row);
198
+ const people = scope.people ? new Set(scope.people) : null;
199
+ if (grant.source === "first_email") {
200
+ const row = rows.get(grant.ref);
201
+ const refusal = evidenceRowRefusal(row, grant, scope, people);
202
+ if (refusal) return `The email this segment was turned on from ${refusal}.`;
203
+ if (row.reviewed_by !== grant.by) return "The email this segment was turned on from was approved by someone else.";
204
+ return null;
205
+ }
206
+ const receipts = Array.from(new Set(grant.receipts ?? []));
207
+ const needed = Math.max(1, Math.ceil(Number(scope.minMeasured) || 0));
208
+ if (receipts.length < needed) {
209
+ return `Only ${receipts.length} of the ${needed} approvals this segment needs to earn its own sending are on record.`;
210
+ }
211
+ for (const id of receipts) {
212
+ const refusal = evidenceRowRefusal(rows.get(id), grant, scope, people);
213
+ if (refusal) return `One of the approvals this segment earned its sending with ${refusal}.`;
214
+ }
215
+ return null;
216
+ }
217
+ var GRANTER_ROLES = /* @__PURE__ */ new Set(["owner", "admin"]);
218
+ function segmentGranterEligible(roles) {
219
+ if (!roles) return false;
220
+ if (typeof roles.contact_type === "string" && roles.contact_type.trim().toLowerCase() === "agent") return false;
221
+ if (roles.is_platform_admin === true) return true;
222
+ return [roles.org_role, roles.project_role, roles.managing_org_role].some(
223
+ (role) => typeof role === "string" && GRANTER_ROLES.has(role.trim().toLowerCase())
224
+ );
225
+ }
226
+ var PUBLIC_MAILBOX_DOMAINS = [
227
+ "gmail.com",
228
+ "googlemail.com",
229
+ "yahoo.com",
230
+ "ymail.com",
231
+ "rocketmail.com",
232
+ "hotmail.com",
233
+ "outlook.com",
234
+ "live.com",
235
+ "msn.com",
236
+ "aol.com",
237
+ "aim.com",
238
+ "icloud.com",
239
+ "me.com",
240
+ "mac.com",
241
+ "protonmail.com",
242
+ "proton.me",
243
+ "pm.me",
244
+ "gmx.com",
245
+ "gmx.net",
246
+ "mail.com",
247
+ "zoho.com",
248
+ "zohomail.com",
249
+ "yandex.com",
250
+ "fastmail.com",
251
+ "hey.com",
252
+ "tutanota.com",
253
+ "tuta.io",
254
+ "att.net",
255
+ "sbcglobal.net",
256
+ "bellsouth.net",
257
+ "comcast.net",
258
+ "verizon.net",
259
+ "cox.net",
260
+ "charter.net",
261
+ "earthlink.net",
262
+ "frontier.com",
263
+ "windstream.net",
264
+ "centurylink.net",
265
+ "optonline.net",
266
+ "juno.com",
267
+ "netzero.net",
268
+ "roadrunner.com",
269
+ "rr.com",
270
+ "qq.com",
271
+ "163.com",
272
+ "126.com",
273
+ "mail.ru",
274
+ "web.de",
275
+ "t-online.de",
276
+ "orange.fr",
277
+ "free.fr",
278
+ "btinternet.com",
279
+ "sky.com",
280
+ "rogers.com",
281
+ "shaw.ca",
282
+ "sympatico.ca",
283
+ "bigpond.com"
284
+ ];
285
+ var PUBLIC_DOMAIN_SET = new Set(PUBLIC_MAILBOX_DOMAINS);
286
+ var PUBLIC_DOMAIN_FAMILY = /^(?:yahoo|ymail|hotmail|live|outlook|msn|aol|gmx|yandex)\.(?:[a-z]{2,3}|co\.[a-z]{2}|com\.[a-z]{2})$/;
287
+ var PUBLIC_DOMAIN_SUFFIX = [".rr.com"];
288
+ function isPublicMailboxDomain(value) {
289
+ if (typeof value !== "string") return false;
290
+ let domain = value.trim().toLowerCase().replace(/^mailto:/, "");
291
+ const at = domain.lastIndexOf("@");
292
+ if (at >= 0) domain = domain.slice(at + 1);
293
+ domain = domain.replace(/\.+$/, "").replace(/^www\./, "");
294
+ if (!domain) return false;
295
+ return PUBLIC_DOMAIN_SET.has(domain) || PUBLIC_DOMAIN_FAMILY.test(domain) || PUBLIC_DOMAIN_SUFFIX.some((suffix) => domain.endsWith(suffix));
296
+ }
297
+ function dripRowDisposition(row) {
298
+ const payload = isRecord(row?.payload) ? row.payload : {};
299
+ const execution = isRecord(payload._execution) ? payload._execution : {};
300
+ switch (row?.status) {
301
+ case "proposed":
302
+ case "approved":
303
+ return "live";
304
+ case "applied":
305
+ case "auto_applied":
306
+ case "executed":
307
+ case "reverted":
308
+ return "sent";
309
+ case "failed":
310
+ return execution.sent === true ? "sent" : "refile";
311
+ case "rejected": {
312
+ const decision = isRecord(row?.autonomy_decision) ? row.autonomy_decision : {};
313
+ if (decision.allowed === "off" && !row?.reviewed_by) return "refile";
314
+ const reason = row?.review_reason;
315
+ if (reason === "wrong_recipient") return "retire";
316
+ if (reason === "wrong_timing" || reason === "withdrawn") return "refile";
317
+ return "declined";
318
+ }
319
+ case "expired":
320
+ case "superseded":
321
+ case "discarded":
322
+ return "refile";
323
+ default:
324
+ return "live";
325
+ }
326
+ }
327
+
328
+ export { GRADUATION_BAR, NARRATIVE_PITCH_FIELDS, OUTREACH_CONTRACT_VERSION, OUTREACH_EMAIL_KIND, OUTREACH_WRITER_BADGE, PUBLIC_MAILBOX_DOMAINS, SEGMENT_STATES, UNSPECIFIED_INDUSTRY, activeSegmentGrant, dripRowDisposition, grantEvidenceRefusal, isGrantableSegmentKey, isPublicMailboxDomain, narrativeFingerprint, normalizeIndustry, outreachSegment, outreachSegmentLabel, parseOutreachSegmentKey, prospectIndustry, readSegmentGrants, segmentGrantCovers, segmentGranterEligible };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sonordev/contracts",
3
- "version": "2.7.1",
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, and which Sonor records Signal reads into a project's knowledge and the address each one is stored under. Pure functions, no dependencies.",
3
+ "version": "2.8.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, and what a segment of outreach is and who may turn one on. Pure functions, no dependencies.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -115,6 +115,16 @@
115
115
  "types": "./dist/editorial-media.d.ts",
116
116
  "import": "./dist/editorial-media.mjs",
117
117
  "require": "./dist/editorial-media.js"
118
+ },
119
+ "./business-claims": {
120
+ "types": "./dist/business-claims.d.ts",
121
+ "import": "./dist/business-claims.mjs",
122
+ "require": "./dist/business-claims.js"
123
+ },
124
+ "./outreach": {
125
+ "types": "./dist/outreach.d.ts",
126
+ "import": "./dist/outreach.mjs",
127
+ "require": "./dist/outreach.js"
118
128
  }
119
129
  },
120
130
  "typesVersions": {