@sonordev/contracts 2.16.0 → 2.17.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 +6 -0
- package/README.md +59 -0
- package/dist/crm-disposition.d.ts +87 -0
- package/dist/crm-disposition.js +79 -0
- package/dist/crm-disposition.mjs +69 -0
- package/package.json +7 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.17.0
|
|
4
|
+
|
|
5
|
+
A minor release: one new entry, `@sonordev/contracts/crm-disposition`. Every other entry is unchanged.
|
|
6
|
+
|
|
7
|
+
- **What a rep's call outcome changes.** `SALES_DISPOSITIONS` is the three outcomes a rep taps after a call (`positive`, `negative`, `voicemail`), `NEGATIVE_REASONS` the five reasons a negative one carries (`not_a_fit`, `no_budget`, `has_agency`, `wrong_number`, `do_not_call`) and `SALES_DISPOSITION_LABELS` / `NEGATIVE_REASON_LABELS` what each button says. `dispositionTransitions(disposition, { reason? })` returns what the outcome does to the call (`outcomeType`, `sentiment`), the contact (`contactStage`), the company (`companyStatus`), today's reminder (`reminder`) and outreach (`stopsOutreach`, `suppresses`). A caller makes the moves; the contract only names them, so the API, the dashboard and the sales extension read one map. `dispositionProblem(disposition, { reason? })` says in a sentence why an outcome can't be applied, such as a negative one with no reason. `isSalesDisposition`, `isNegativeReason` and `CRM_DISPOSITION_CONTRACT_VERSION` (1) complete it.
|
|
8
|
+
|
|
3
9
|
## 2.16.0
|
|
4
10
|
|
|
5
11
|
A minor release: `@sonordev/contracts/portfolio` gains the verification, a proof-stage section for builds a Lighthouse run can't honestly score. Every other entry is unchanged. Nothing was removed or renamed, but three things behave differently. `PortfolioSectionType` has a new member, so a `Record<PortfolioSectionType, ...>` you keep needs an entry for `portfolioVerification` before it type-checks. `caseStudyStageStatus` reads each section's `data`. A reported number's date has to be a real day.
|
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@ pnpm add @sonordev/contracts
|
|
|
40
40
|
| `@sonordev/contracts/public-url-host` | Which literal hosts are refused as public fetch destinations (`isBlockedPublicUrlHost`) | the Sonor API and Signal's URL validators and network fetchers |
|
|
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
|
+
| `@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 |
|
|
43
44
|
|
|
44
45
|
## Focus closing artwork
|
|
45
46
|
|
|
@@ -90,6 +91,64 @@ list (a database filter that leaves them out of a bulk write, for one).
|
|
|
90
91
|
key, a JSON `null` and a `null` `data` mean no proposal, and any other value,
|
|
91
92
|
an empty string included, means one.
|
|
92
93
|
|
|
94
|
+
## Call dispositions
|
|
95
|
+
|
|
96
|
+
After a sales call a rep taps one of three outcomes. What each one changes on
|
|
97
|
+
the prospect is written here once, so the API that applies it, the calling
|
|
98
|
+
screen that offers it and the browser extension that dials from a list can't
|
|
99
|
+
disagree about what "negative" moves.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { dispositionProblem, dispositionTransitions } from '@sonordev/contracts/crm-disposition'
|
|
103
|
+
|
|
104
|
+
dispositionProblem('negative', { reason: undefined })
|
|
105
|
+
// 'A negative outcome needs a reason: not_a_fit, no_budget, has_agency, wrong_number, do_not_call.'
|
|
106
|
+
|
|
107
|
+
dispositionTransitions('negative', { reason: 'do_not_call' })
|
|
108
|
+
// {
|
|
109
|
+
// outcomeType: 'connected', sentiment: 'negative',
|
|
110
|
+
// contactStage: 'lost', companyStatus: 'not_a_fit',
|
|
111
|
+
// reminder: 'complete', stopsOutreach: false, suppresses: true,
|
|
112
|
+
// }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
| Outcome | Call | Contact stage | Company status | Today's reminder | Outreach |
|
|
116
|
+
|---|---|---|---|---|---|
|
|
117
|
+
| `positive` | `connected`, positive | `contacted` | `contacted` | `complete` | the drip stops (`stopsOutreach`) |
|
|
118
|
+
| `negative` | `connected`, negative | `lost` | `not_a_fit` | `complete` | nothing, unless the reason is `do_not_call` (`suppresses`) |
|
|
119
|
+
| `voicemail` | `left_voicemail`, no sentiment | unchanged | unchanged | `snooze_next_business_day` | nothing |
|
|
120
|
+
|
|
121
|
+
- **The contract names the moves; the caller makes them.** `contactStage` is a
|
|
122
|
+
meaning (`contacted` or `lost`), not a project's stage key. `reminder:
|
|
123
|
+
'snooze_next_business_day'` leaves the instant to the caller, who knows the
|
|
124
|
+
project's time zone. A `null` in any field means leave that thing as it is.
|
|
125
|
+
A stage a rep picks by hand afterwards, such as `qualified`, is a separate
|
|
126
|
+
move that no outcome makes.
|
|
127
|
+
- **A voicemail isn't a conversation.** It leaves the contact and company
|
|
128
|
+
alone, and a company only moves to `contacted` when someone answered. Dialing
|
|
129
|
+
a number moves nothing either; the outcome does.
|
|
130
|
+
- **A negative outcome needs a reason.** `NEGATIVE_REASONS` is the five, in the
|
|
131
|
+
order a screen offers them: `not_a_fit`, `no_budget`, `has_agency`,
|
|
132
|
+
`wrong_number` and `do_not_call`. `NEGATIVE_REASON_LABELS` and
|
|
133
|
+
`SALES_DISPOSITION_LABELS` say what each button reads.
|
|
134
|
+
- **`suppresses` is for `do_not_call` only.** It means write a suppression for
|
|
135
|
+
the prospect, which blocks every send on its own. It's separate from
|
|
136
|
+
`stopsOutreach`, which stops a drip the prospect is enrolled in, so a caller
|
|
137
|
+
that wants both for an "asked not to call" outcome stops the drip as well.
|
|
138
|
+
- **Check an untrusted outcome first.** `dispositionProblem(disposition,
|
|
139
|
+
{ reason })` returns one sentence about why an outcome can't be applied, or
|
|
140
|
+
`null`: a word that isn't one of the three, a negative outcome with no reason
|
|
141
|
+
or one that isn't a listed reason, or a reason on an outcome that takes none.
|
|
142
|
+
`isSalesDisposition` and `isNegativeReason` are the plain guards.
|
|
143
|
+
`dispositionTransitions` throws a `RangeError` for something that isn't a
|
|
144
|
+
disposition, and ignores a reason on any outcome but a negative one.
|
|
145
|
+
- **Fresh each time.** `dispositionTransitions` returns a new object per call,
|
|
146
|
+
so changing one caller's copy never changes the next.
|
|
147
|
+
|
|
148
|
+
`CRM_DISPOSITION_CONTRACT_VERSION` is bumped when an outcome's transitions
|
|
149
|
+
change incompatibly. The wire values (the three outcomes and five reasons) are
|
|
150
|
+
stored and sent, so they don't change.
|
|
151
|
+
|
|
93
152
|
## Schema placeholders
|
|
94
153
|
|
|
95
154
|
`schema-placeholders` finds the parts of a JSON-LD value that a template left
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a rep's three call outcomes do to a prospect. Single source of truth for
|
|
3
|
+
* the Sales Mode disposition: the Sonor API applies it when a rep taps an
|
|
4
|
+
* outcome, the dashboard shows the same three buttons and reasons, and the
|
|
5
|
+
* sales extension stops guessing at 'contacted' when a number is dialed. One
|
|
6
|
+
* map, so the three can't disagree about what "negative" moves.
|
|
7
|
+
*
|
|
8
|
+
* positive a connected call that went well. The contact is `contacted`,
|
|
9
|
+
* the company is `contacted`, today's reminder is done and the
|
|
10
|
+
* prospect's drip stops, since they're now in a conversation.
|
|
11
|
+
* negative a connected call that ended the lead. The contact is `lost`
|
|
12
|
+
* (a reason is required), the company is `not_a_fit`, today's
|
|
13
|
+
* reminder is done. Only "asked not to call" suppresses outreach.
|
|
14
|
+
* voicemail nobody picked up. Nothing about the contact or company changes,
|
|
15
|
+
* because a voicemail isn't a conversation, and today's reminder
|
|
16
|
+
* moves to the next business day.
|
|
17
|
+
*
|
|
18
|
+
* The contract names the moves; the caller makes them. Which stage key
|
|
19
|
+
* `contacted` or `lost` is on a given project, which instant "the next business
|
|
20
|
+
* day" is in the project's time zone, and how a suppression row is written all
|
|
21
|
+
* stay with the Sonor API. A stage the rep picks by hand afterwards (qualified,
|
|
22
|
+
* say) is a separate move that no disposition makes.
|
|
23
|
+
*
|
|
24
|
+
* Pure data and a few small functions, no dependencies. The wire values (the
|
|
25
|
+
* three dispositions, the five reasons) are stored and sent, so they never
|
|
26
|
+
* change; a different map is a new CRM_DISPOSITION_CONTRACT_VERSION.
|
|
27
|
+
*/
|
|
28
|
+
/** Bumped when a disposition's transitions change incompatibly. */
|
|
29
|
+
export declare const CRM_DISPOSITION_CONTRACT_VERSION = 1;
|
|
30
|
+
/** The three words a rep taps after a call, in button order. */
|
|
31
|
+
export declare const SALES_DISPOSITIONS: readonly ['positive', 'negative', 'voicemail'];
|
|
32
|
+
export type SalesDisposition = (typeof SALES_DISPOSITIONS)[number];
|
|
33
|
+
/** What each button says. */
|
|
34
|
+
export declare const SALES_DISPOSITION_LABELS: Record<SalesDisposition, string>;
|
|
35
|
+
/** The one-tap reasons a negative outcome must carry, in the order they're offered. */
|
|
36
|
+
export declare const NEGATIVE_REASONS: readonly ['not_a_fit', 'no_budget', 'has_agency', 'wrong_number', 'do_not_call'];
|
|
37
|
+
export type NegativeReason = (typeof NEGATIVE_REASONS)[number];
|
|
38
|
+
/** What each reason says on its button. `do_not_call` is the one that suppresses outreach. */
|
|
39
|
+
export declare const NEGATIVE_REASON_LABELS: Record<NegativeReason, string>;
|
|
40
|
+
/** Whether a value is one of the three dispositions, spelled exactly. */
|
|
41
|
+
export declare function isSalesDisposition(value: unknown): value is SalesDisposition;
|
|
42
|
+
/** Whether a value is one of the five negative reasons, spelled exactly. */
|
|
43
|
+
export declare function isNegativeReason(value: unknown): value is NegativeReason;
|
|
44
|
+
/** What a disposition changes. A null leaves that thing as it is. */
|
|
45
|
+
export interface DispositionTransitions {
|
|
46
|
+
/** The call row's `outcome_type`. */
|
|
47
|
+
outcomeType: 'connected' | 'left_voicemail';
|
|
48
|
+
/** The call row's `sentiment`. */
|
|
49
|
+
sentiment: 'positive' | 'negative' | null;
|
|
50
|
+
/** The contact's stage, by meaning rather than by a project's stage key. */
|
|
51
|
+
contactStage: 'contacted' | 'lost' | null;
|
|
52
|
+
/** The target company's `status`. */
|
|
53
|
+
companyStatus: 'contacted' | 'not_a_fit' | null;
|
|
54
|
+
/**
|
|
55
|
+
* What happens to the prospect's reminder due today: finish it, or push it to
|
|
56
|
+
* 09:00 on the project's next business day. No disposition leaves it alone
|
|
57
|
+
* today; a caller still handles null so that never has to be a breaking change.
|
|
58
|
+
*/
|
|
59
|
+
reminder: 'complete' | 'snooze_next_business_day' | null;
|
|
60
|
+
/** Stop the prospect's enrolled drip or sequence. */
|
|
61
|
+
stopsOutreach: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Write an outreach suppression for the prospect. That blocks every send on
|
|
64
|
+
* its own, so it isn't also a `stopsOutreach`. Only a negative outcome whose
|
|
65
|
+
* reason is `do_not_call` suppresses.
|
|
66
|
+
*/
|
|
67
|
+
suppresses: boolean;
|
|
68
|
+
}
|
|
69
|
+
export interface DispositionOptions {
|
|
70
|
+
/** The negative outcome's reason. Read for a negative outcome only. */
|
|
71
|
+
reason?: NegativeReason | null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* What a disposition changes. Returns a fresh object each call, so a caller can
|
|
75
|
+
* adjust its copy without touching the next caller's. Throws a RangeError for a
|
|
76
|
+
* value that isn't a disposition: check an untrusted one with
|
|
77
|
+
* `dispositionProblem` first.
|
|
78
|
+
*/
|
|
79
|
+
export declare function dispositionTransitions(disposition: SalesDisposition, opts?: DispositionOptions): DispositionTransitions;
|
|
80
|
+
/**
|
|
81
|
+
* Why a disposition and its reason can't be applied, in one sentence, or null
|
|
82
|
+
* when they can. A negative outcome needs a reason and no other outcome takes
|
|
83
|
+
* one. A null or undefined reason means none was given.
|
|
84
|
+
*/
|
|
85
|
+
export declare function dispositionProblem(disposition: unknown, opts?: {
|
|
86
|
+
reason?: unknown;
|
|
87
|
+
}): string | null;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/contracts/crm-disposition.ts
|
|
4
|
+
var CRM_DISPOSITION_CONTRACT_VERSION = 1;
|
|
5
|
+
var SALES_DISPOSITIONS = ["positive", "negative", "voicemail"];
|
|
6
|
+
var SALES_DISPOSITION_LABELS = {
|
|
7
|
+
positive: "Positive",
|
|
8
|
+
negative: "Negative",
|
|
9
|
+
voicemail: "Left voicemail"
|
|
10
|
+
};
|
|
11
|
+
var NEGATIVE_REASONS = ["not_a_fit", "no_budget", "has_agency", "wrong_number", "do_not_call"];
|
|
12
|
+
var NEGATIVE_REASON_LABELS = {
|
|
13
|
+
not_a_fit: "Not a fit",
|
|
14
|
+
no_budget: "No budget",
|
|
15
|
+
has_agency: "Has an agency",
|
|
16
|
+
wrong_number: "Wrong number",
|
|
17
|
+
do_not_call: "Asked not to call"
|
|
18
|
+
};
|
|
19
|
+
function isSalesDisposition(value) {
|
|
20
|
+
return typeof value === "string" && SALES_DISPOSITIONS.includes(value);
|
|
21
|
+
}
|
|
22
|
+
function isNegativeReason(value) {
|
|
23
|
+
return typeof value === "string" && NEGATIVE_REASONS.includes(value);
|
|
24
|
+
}
|
|
25
|
+
var TRANSITIONS = {
|
|
26
|
+
positive: {
|
|
27
|
+
outcomeType: "connected",
|
|
28
|
+
sentiment: "positive",
|
|
29
|
+
contactStage: "contacted",
|
|
30
|
+
companyStatus: "contacted",
|
|
31
|
+
reminder: "complete",
|
|
32
|
+
stopsOutreach: true
|
|
33
|
+
},
|
|
34
|
+
negative: {
|
|
35
|
+
outcomeType: "connected",
|
|
36
|
+
sentiment: "negative",
|
|
37
|
+
contactStage: "lost",
|
|
38
|
+
companyStatus: "not_a_fit",
|
|
39
|
+
reminder: "complete",
|
|
40
|
+
stopsOutreach: false
|
|
41
|
+
},
|
|
42
|
+
voicemail: {
|
|
43
|
+
outcomeType: "left_voicemail",
|
|
44
|
+
sentiment: null,
|
|
45
|
+
contactStage: null,
|
|
46
|
+
companyStatus: null,
|
|
47
|
+
reminder: "snooze_next_business_day",
|
|
48
|
+
stopsOutreach: false
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
function dispositionTransitions(disposition, opts = {}) {
|
|
52
|
+
if (!isSalesDisposition(disposition)) {
|
|
53
|
+
throw new RangeError(`Not a sales disposition: ${String(disposition)}`);
|
|
54
|
+
}
|
|
55
|
+
return { ...TRANSITIONS[disposition], suppresses: disposition === "negative" && opts.reason === "do_not_call" };
|
|
56
|
+
}
|
|
57
|
+
function dispositionProblem(disposition, opts = {}) {
|
|
58
|
+
if (!isSalesDisposition(disposition)) {
|
|
59
|
+
return `The outcome has to be one of ${SALES_DISPOSITIONS.join(", ")}.`;
|
|
60
|
+
}
|
|
61
|
+
const reason = opts.reason;
|
|
62
|
+
const given = reason !== null && reason !== void 0;
|
|
63
|
+
if (disposition === "negative") {
|
|
64
|
+
if (!given) return `A negative outcome needs a reason: ${NEGATIVE_REASONS.join(", ")}.`;
|
|
65
|
+
if (!isNegativeReason(reason)) return `The reason has to be one of ${NEGATIVE_REASONS.join(", ")}.`;
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return given ? `Only a negative outcome takes a reason, and this one is ${disposition}.` : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
exports.CRM_DISPOSITION_CONTRACT_VERSION = CRM_DISPOSITION_CONTRACT_VERSION;
|
|
72
|
+
exports.NEGATIVE_REASONS = NEGATIVE_REASONS;
|
|
73
|
+
exports.NEGATIVE_REASON_LABELS = NEGATIVE_REASON_LABELS;
|
|
74
|
+
exports.SALES_DISPOSITIONS = SALES_DISPOSITIONS;
|
|
75
|
+
exports.SALES_DISPOSITION_LABELS = SALES_DISPOSITION_LABELS;
|
|
76
|
+
exports.dispositionProblem = dispositionProblem;
|
|
77
|
+
exports.dispositionTransitions = dispositionTransitions;
|
|
78
|
+
exports.isNegativeReason = isNegativeReason;
|
|
79
|
+
exports.isSalesDisposition = isSalesDisposition;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// src/contracts/crm-disposition.ts
|
|
2
|
+
var CRM_DISPOSITION_CONTRACT_VERSION = 1;
|
|
3
|
+
var SALES_DISPOSITIONS = ["positive", "negative", "voicemail"];
|
|
4
|
+
var SALES_DISPOSITION_LABELS = {
|
|
5
|
+
positive: "Positive",
|
|
6
|
+
negative: "Negative",
|
|
7
|
+
voicemail: "Left voicemail"
|
|
8
|
+
};
|
|
9
|
+
var NEGATIVE_REASONS = ["not_a_fit", "no_budget", "has_agency", "wrong_number", "do_not_call"];
|
|
10
|
+
var NEGATIVE_REASON_LABELS = {
|
|
11
|
+
not_a_fit: "Not a fit",
|
|
12
|
+
no_budget: "No budget",
|
|
13
|
+
has_agency: "Has an agency",
|
|
14
|
+
wrong_number: "Wrong number",
|
|
15
|
+
do_not_call: "Asked not to call"
|
|
16
|
+
};
|
|
17
|
+
function isSalesDisposition(value) {
|
|
18
|
+
return typeof value === "string" && SALES_DISPOSITIONS.includes(value);
|
|
19
|
+
}
|
|
20
|
+
function isNegativeReason(value) {
|
|
21
|
+
return typeof value === "string" && NEGATIVE_REASONS.includes(value);
|
|
22
|
+
}
|
|
23
|
+
var TRANSITIONS = {
|
|
24
|
+
positive: {
|
|
25
|
+
outcomeType: "connected",
|
|
26
|
+
sentiment: "positive",
|
|
27
|
+
contactStage: "contacted",
|
|
28
|
+
companyStatus: "contacted",
|
|
29
|
+
reminder: "complete",
|
|
30
|
+
stopsOutreach: true
|
|
31
|
+
},
|
|
32
|
+
negative: {
|
|
33
|
+
outcomeType: "connected",
|
|
34
|
+
sentiment: "negative",
|
|
35
|
+
contactStage: "lost",
|
|
36
|
+
companyStatus: "not_a_fit",
|
|
37
|
+
reminder: "complete",
|
|
38
|
+
stopsOutreach: false
|
|
39
|
+
},
|
|
40
|
+
voicemail: {
|
|
41
|
+
outcomeType: "left_voicemail",
|
|
42
|
+
sentiment: null,
|
|
43
|
+
contactStage: null,
|
|
44
|
+
companyStatus: null,
|
|
45
|
+
reminder: "snooze_next_business_day",
|
|
46
|
+
stopsOutreach: false
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
function dispositionTransitions(disposition, opts = {}) {
|
|
50
|
+
if (!isSalesDisposition(disposition)) {
|
|
51
|
+
throw new RangeError(`Not a sales disposition: ${String(disposition)}`);
|
|
52
|
+
}
|
|
53
|
+
return { ...TRANSITIONS[disposition], suppresses: disposition === "negative" && opts.reason === "do_not_call" };
|
|
54
|
+
}
|
|
55
|
+
function dispositionProblem(disposition, opts = {}) {
|
|
56
|
+
if (!isSalesDisposition(disposition)) {
|
|
57
|
+
return `The outcome has to be one of ${SALES_DISPOSITIONS.join(", ")}.`;
|
|
58
|
+
}
|
|
59
|
+
const reason = opts.reason;
|
|
60
|
+
const given = reason !== null && reason !== void 0;
|
|
61
|
+
if (disposition === "negative") {
|
|
62
|
+
if (!given) return `A negative outcome needs a reason: ${NEGATIVE_REASONS.join(", ")}.`;
|
|
63
|
+
if (!isNegativeReason(reason)) return `The reason has to be one of ${NEGATIVE_REASONS.join(", ")}.`;
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return given ? `Only a negative outcome takes a reason, and this one is ${disposition}.` : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { CRM_DISPOSITION_CONTRACT_VERSION, NEGATIVE_REASONS, NEGATIVE_REASON_LABELS, SALES_DISPOSITIONS, SALES_DISPOSITION_LABELS, dispositionProblem, dispositionTransitions, isNegativeReason, isSalesDisposition };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sonordev/contracts",
|
|
3
|
-
"version": "2.
|
|
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,
|
|
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.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -155,6 +155,11 @@
|
|
|
155
155
|
"types": "./dist/funnels.d.ts",
|
|
156
156
|
"import": "./dist/funnels.mjs",
|
|
157
157
|
"require": "./dist/funnels.js"
|
|
158
|
+
},
|
|
159
|
+
"./crm-disposition": {
|
|
160
|
+
"types": "./dist/crm-disposition.d.ts",
|
|
161
|
+
"import": "./dist/crm-disposition.mjs",
|
|
162
|
+
"require": "./dist/crm-disposition.js"
|
|
158
163
|
}
|
|
159
164
|
},
|
|
160
165
|
"typesVersions": {
|