@oxygen-agent/cli 1.1011.97 → 1.1011.288
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/README.md +1 -1
- package/dist/admin-primary-providers-render.js +25 -5
- package/dist/column-run-notices.d.ts +16 -0
- package/dist/column-run-notices.js +57 -0
- package/dist/command-manifest.js +10 -1
- package/dist/help.d.ts +27 -1
- package/dist/help.js +112 -29
- package/dist/http-client.js +8 -0
- package/dist/index.js +348 -127
- package/node_modules/@oxygen/cli-ugc/dist/commands.js +20 -69
- package/node_modules/@oxygen/shared/dist/byok-connect.d.ts +6 -4
- package/node_modules/@oxygen/shared/dist/byok-connect.js +11 -4
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +127 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +286 -24
- package/node_modules/@oxygen/shared/dist/collab.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/collab.js +12 -0
- package/node_modules/@oxygen/shared/dist/column-decision.js +7 -3
- package/node_modules/@oxygen/shared/dist/column-output-fields.js +45 -3
- package/node_modules/@oxygen/shared/dist/company-enrichment-fields.d.ts +20 -13
- package/node_modules/@oxygen/shared/dist/company-enrichment-fields.js +29 -13
- package/node_modules/@oxygen/shared/dist/contracts/sender-limits.js +1 -1
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +2 -2
- package/node_modules/@oxygen/shared/dist/credit-guidance.js +12 -1
- package/node_modules/@oxygen/shared/dist/enrichment-intents.d.ts +37 -0
- package/node_modules/@oxygen/shared/dist/enrichment-intents.js +79 -44
- package/node_modules/@oxygen/shared/dist/linkedin-url.d.ts +7 -0
- package/node_modules/@oxygen/shared/dist/linkedin-url.js +9 -0
- package/node_modules/@oxygen/shared/dist/log.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/log.js +15 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.js +6 -2
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +7 -3
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +22 -1
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +3 -7
- package/node_modules/@oxygen/shared/dist/plan-limits.js +2 -14
- package/node_modules/@oxygen/shared/dist/sending-schedule.d.ts +39 -0
- package/node_modules/@oxygen/shared/dist/sending-schedule.js +41 -0
- package/node_modules/@oxygen/shared/dist/sequence-lifecycle.d.ts +19 -0
- package/node_modules/@oxygen/shared/dist/sequence-lifecycle.js +20 -0
- package/node_modules/@oxygen/shared/dist/sequence-sender-variables.d.ts +35 -0
- package/node_modules/@oxygen/shared/dist/sequence-sender-variables.js +63 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +13 -7
- package/node_modules/@oxygen/shared/dist/sequences.js +26 -0
- package/node_modules/@oxygen/shared/dist/signup-lead-payload.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/signup-lead-payload.js +5 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-series.js +1 -15
- package/node_modules/@oxygen/shared/dist/standard-webhooks.d.ts +51 -0
- package/node_modules/@oxygen/shared/dist/standard-webhooks.js +124 -0
- package/node_modules/@oxygen/shared/dist/tags.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/tags.js +13 -1
- package/node_modules/@oxygen/shared/dist/ugc.d.ts +11 -7
- package/node_modules/@oxygen/shared/dist/ugc.js +14 -6
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/zoned-time.d.ts +39 -0
- package/node_modules/@oxygen/shared/dist/zoned-time.js +81 -0
- package/package.json +1 -1
|
@@ -31,6 +31,8 @@ export const ENRICHMENT_INTENT_GROUP_LABELS = {
|
|
|
31
31
|
signal: "Signals",
|
|
32
32
|
};
|
|
33
33
|
const f = (key, label, semanticType, dataType = "text", derived) => ({ key, label, semanticType, dataType, ...(derived ? { derived: true } : {}) });
|
|
34
|
+
/** A field read out of one column's cell, e.g. the email in `work_email_enrichment.value`. */
|
|
35
|
+
const cell = (key, path, label, semanticType, dataType = "text") => ({ key, path, label, semanticType, dataType });
|
|
34
36
|
export const ENRICHMENT_INTENT_TAXONOMY = [
|
|
35
37
|
// ── Person ────────────────────────────────────────────────────────────────
|
|
36
38
|
{
|
|
@@ -48,13 +50,13 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
|
|
|
48
50
|
f("first_name", "First name", "person.first_name"),
|
|
49
51
|
f("last_name", "Last name", "person.last_name"),
|
|
50
52
|
f("job_title", "Job title", "person.job_title"),
|
|
51
|
-
f("
|
|
53
|
+
f("person_headline", "Headline", "person.headline"),
|
|
52
54
|
f("current_company", "Current company", null),
|
|
53
55
|
f("current_company_linkedin_url", "Current company LinkedIn URL", "company.linkedin_url"),
|
|
54
56
|
f("job_start_date", "Job start date", null),
|
|
55
|
-
f("
|
|
56
|
-
f("
|
|
57
|
-
f("
|
|
57
|
+
f("person_location", "Location", "person.location"),
|
|
58
|
+
f("person_bio", "Bio", "person.bio"),
|
|
59
|
+
f("person_linkedin_followers", "LinkedIn followers", "person.linkedin_followers", "numeric"),
|
|
58
60
|
f("education", "Education", null, "jsonb"),
|
|
59
61
|
f("school_name", "School", null),
|
|
60
62
|
],
|
|
@@ -73,8 +75,9 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
|
|
|
73
75
|
aliases: ["linkedin url", "linkedin urls", "find linkedin", "profile url", "resolve linkedin", "linkedin for each"],
|
|
74
76
|
inputShapes: ["name_company"],
|
|
75
77
|
outputFields: [
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
cell("linkedin_url_enrichment", "value", "LinkedIn URL", "person.linkedin_url"),
|
|
79
|
+
// The identity check's verdict on the hit (D46): a URL that fails it is a miss.
|
|
80
|
+
cell("linkedin_url_enrichment", "confidence", "Match confidence", null),
|
|
78
81
|
],
|
|
79
82
|
surfaces: { capability: "linkedin_url", find: "find linkedin", managedFunction: "person_linkedin_url", catalogEntry: "capability:linkedin_url", registryFunction: "enrichment", registryIntent: "find_linkedin_profile" },
|
|
80
83
|
status: "default",
|
|
@@ -83,7 +86,7 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
|
|
|
83
86
|
{
|
|
84
87
|
id: "person_company",
|
|
85
88
|
label: "Person's company",
|
|
86
|
-
description: "Adds the person's current company, then fills that company's domain, industry and headcount
|
|
89
|
+
description: "Adds the person's current company from their profile, then fills that company's domain, industry and headcount.",
|
|
87
90
|
entity: "person",
|
|
88
91
|
group: "person",
|
|
89
92
|
kind: "template",
|
|
@@ -94,8 +97,10 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
|
|
|
94
97
|
f("current_company_linkedin_url", "Company LinkedIn URL", "company.linkedin_url"),
|
|
95
98
|
f("company_domain", "Company domain", "company.domain"),
|
|
96
99
|
f("company_industry", "Industry", "company.industry"),
|
|
97
|
-
|
|
100
|
+
// Text: the exact count when the lookup has one, otherwise the size band.
|
|
101
|
+
f("company_headcount", "Headcount", "company.employee_count"),
|
|
98
102
|
],
|
|
103
|
+
// The recipe's two preset commands are this row's add path.
|
|
99
104
|
surfaces: { catalogEntry: "recipe:person_company" },
|
|
100
105
|
status: "default",
|
|
101
106
|
},
|
|
@@ -111,11 +116,22 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
|
|
|
111
116
|
aliases: ["work email", "business email", "email address", "find email", "emails for", "email finder", "with emails"],
|
|
112
117
|
inputShapes: ["linkedin_url", "name_domain", "first_last_domain"],
|
|
113
118
|
outputFields: [
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
cell("work_email_enrichment", "value", "Work email", "person.work_email"),
|
|
120
|
+
// verified | catch_all | guessed | unverified, the grade the hit landed with.
|
|
121
|
+
cell("work_email_enrichment", "confidence", "Deliverability", "person.email_status"),
|
|
116
122
|
],
|
|
117
123
|
surfaces: { capability: "work_email", find: "find email", managedFunction: "work_email", catalogEntry: "capability:work_email", registryFunction: "enrichment", registryIntent: "find_work_email" },
|
|
118
124
|
status: "default",
|
|
125
|
+
// Founder, 2026-09-29: one email column; personal email is its option.
|
|
126
|
+
options: [
|
|
127
|
+
{
|
|
128
|
+
id: "include_personal_email",
|
|
129
|
+
intent: "find_personal_email",
|
|
130
|
+
label: "Personal email",
|
|
131
|
+
cliFlag: "--include-personal-email",
|
|
132
|
+
outputFields: [cell("work_email_enrichment", "personal_email.value", "Personal email", "person.personal_email")],
|
|
133
|
+
},
|
|
134
|
+
],
|
|
119
135
|
},
|
|
120
136
|
{
|
|
121
137
|
id: "find_personal_email",
|
|
@@ -127,10 +143,17 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
|
|
|
127
143
|
rank: 50,
|
|
128
144
|
aliases: ["personal email", "gmail", "home email", "private email", "consumer email"],
|
|
129
145
|
inputShapes: ["linkedin_url"],
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
146
|
+
// The address lands in the Work email column's cell: the option is this
|
|
147
|
+
// row's add path.
|
|
148
|
+
outputFields: [cell("work_email_enrichment", "personal_email.value", "Personal email", "person.personal_email")],
|
|
149
|
+
// No column of its own (founder, 2026-09-29): the Work email column's
|
|
150
|
+
// `include_personal_email` option is its add path. `--capability
|
|
151
|
+
// personal_email` still seeds one for scripts that used it.
|
|
152
|
+
surfaces: { find: "find personal-email", managedFunction: "personal_email", catalogEntry: "capability:personal_email", registryFunction: "enrichment", registryIntent: "find_personal_email" },
|
|
153
|
+
status: "selectable",
|
|
154
|
+
statusReason: "An option of the Work email column rather than a column of its own: switch on Personal email when you add or edit Work email.",
|
|
155
|
+
decision: "D59",
|
|
156
|
+
optionOf: "find_work_email",
|
|
134
157
|
},
|
|
135
158
|
{
|
|
136
159
|
id: "find_phone",
|
|
@@ -142,12 +165,10 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
|
|
|
142
165
|
rank: 60,
|
|
143
166
|
aliases: ["mobile number", "mobile phone", "cell phone", "direct dial", "work phone", "phone number", "phone numbers", "this person's mobile", "find phone", "find mobile", "a mobile"],
|
|
144
167
|
inputShapes: ["linkedin_url", "email", "name_domain"],
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
f("phone_carrier", "Carrier", null),
|
|
150
|
-
],
|
|
168
|
+
// One number per row. Line type and carrier are the optional verification
|
|
169
|
+
// step's (`enrich-column run --verify-phone`), written as companion columns
|
|
170
|
+
// that run creates; the column this row's add path creates never holds them.
|
|
171
|
+
outputFields: [cell("mobile_phone_enrichment", "value", "Mobile phone", "person.mobile_phone")],
|
|
151
172
|
surfaces: { capability: "mobile_phone", find: "find phone", managedFunction: "mobile_phone", catalogEntry: "capability:mobile_phone", registryFunction: "enrichment", registryIntent: "find_mobile_phone" },
|
|
152
173
|
status: "default",
|
|
153
174
|
decision: "D44",
|
|
@@ -162,7 +183,8 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
|
|
|
162
183
|
rank: 70,
|
|
163
184
|
aliases: ["verify email", "verify these emails", "validate email", "email verification", "deliverability", "bounce check", "catch-all"],
|
|
164
185
|
inputShapes: ["email"],
|
|
165
|
-
|
|
186
|
+
// The verdict is the cell's value: valid, invalid or catch_all (unknown writes none).
|
|
187
|
+
outputFields: [cell("email_verification", "value", "Verdict", "person.email_status")],
|
|
166
188
|
surfaces: { capability: "verify_email", find: "verify email", catalogEntry: "capability:verify_email", registryFunction: "enrichment", registryIntent: "verify_email" },
|
|
167
189
|
status: "default",
|
|
168
190
|
decision: "D45",
|
|
@@ -177,14 +199,17 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
|
|
|
177
199
|
rank: 80,
|
|
178
200
|
aliases: ["verify phone", "verify these phone numbers", "validate phone", "line type", "is this a mobile"],
|
|
179
201
|
inputShapes: ["phone"],
|
|
202
|
+
// The companions the Phone column's verification step writes; the verdict
|
|
203
|
+
// only gates whether a found number is kept, and nothing stores it.
|
|
180
204
|
outputFields: [
|
|
181
205
|
f("phone_line_type", "Line type", null),
|
|
182
206
|
f("phone_carrier", "Carrier", null),
|
|
183
|
-
f("phone_verdict", "Verdict", null),
|
|
184
207
|
],
|
|
185
|
-
surfaces: {
|
|
186
|
-
status: "
|
|
187
|
-
statusReason: "
|
|
208
|
+
surfaces: {},
|
|
209
|
+
status: "coming_soon",
|
|
210
|
+
statusReason: "Coming soon: checking numbers you already hold is not a column yet. The Phone column's optional verification step adds line type and carrier to the numbers it finds.",
|
|
211
|
+
decision: "D59",
|
|
212
|
+
candidateProviders: ["clearoutphone"],
|
|
188
213
|
},
|
|
189
214
|
// ── Company ───────────────────────────────────────────────────────────────
|
|
190
215
|
{
|
|
@@ -205,14 +230,15 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
|
|
|
205
230
|
{
|
|
206
231
|
id: "enrich_company:headcount",
|
|
207
232
|
label: "Headcount",
|
|
208
|
-
description: "Current employee count
|
|
233
|
+
description: "Current employee count, or the size band when no exact count is published.",
|
|
209
234
|
entity: "company",
|
|
210
235
|
group: "company",
|
|
211
236
|
kind: "enrichment",
|
|
212
237
|
rank: 110,
|
|
213
238
|
aliases: ["headcount", "employee count", "how many employees", "company size", "employees"],
|
|
214
239
|
inputShapes: ["domain", "linkedin_url"],
|
|
215
|
-
|
|
240
|
+
// One text column: the exact count when the lookup has one, otherwise the band.
|
|
241
|
+
outputFields: [f("company_headcount", "Headcount", "company.employee_count")],
|
|
216
242
|
surfaces: { preset: "company_enrich", companyFields: ["headcount"], managedFunction: "company_employee_count", catalogEntry: "preset:company_enrich", registryFunction: "enrichment", registryIntent: "enrich_company", registryInputShapes: ["field:headcount"] },
|
|
217
243
|
status: "default",
|
|
218
244
|
decision: "D48",
|
|
@@ -235,22 +261,21 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
|
|
|
235
261
|
{
|
|
236
262
|
id: "enrich_company:company_profile",
|
|
237
263
|
label: "Company profile",
|
|
238
|
-
description: "Description, founded year
|
|
264
|
+
description: "Description, founded year and headquarters country from one profile lookup, with the whole profile kept alongside.",
|
|
239
265
|
entity: "company",
|
|
240
266
|
group: "company",
|
|
241
267
|
kind: "profile",
|
|
242
268
|
rank: 130,
|
|
243
269
|
aliases: ["company profile", "company description", "founded year", "headquarters", "company type", "specialties", "company logo", "firmographics"],
|
|
244
270
|
inputShapes: ["domain", "linkedin_url"],
|
|
271
|
+
// The company bundle's own columns (D56): its payload column holds the
|
|
272
|
+
// whole profile; HQ address, type, specialties and logo have no column of
|
|
273
|
+
// their own since the bundle stopped taking field choices.
|
|
245
274
|
outputFields: [
|
|
246
|
-
f("
|
|
275
|
+
f("company_enrich", "Company profile", null, "jsonb"),
|
|
247
276
|
f("company_description", "Description", "company.description", "text", true),
|
|
248
277
|
f("company_founded_year", "Founded year", "company.founded_year", "numeric", true),
|
|
249
|
-
f("company_hq_address", "HQ address", "company.hq_location", "text", true),
|
|
250
278
|
f("company_hq_country", "HQ country", "company.hq_country", "text", true),
|
|
251
|
-
f("company_type", "Company type", null, "text", true),
|
|
252
|
-
f("company_specialties", "Specialties", "company.specialties", "jsonb", true),
|
|
253
|
-
f("company_logo_url", "Logo", "company.logo_url", "text", true),
|
|
254
279
|
],
|
|
255
280
|
surfaces: { preset: "company_enrich", companyFields: ["company_profile", "description", "founded_year", "hq_address", "hq_country", "company_type", "specialties", "logo_url"], managedFunction: "enrich_company", catalogEntry: "preset:company_enrich", registryFunction: "enrichment", registryIntent: "enrich_company", registryInputShapes: ["field:company_profile"] },
|
|
256
281
|
status: "default",
|
|
@@ -275,22 +300,26 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
|
|
|
275
300
|
f("company_investors", "Investors", null, "jsonb", true),
|
|
276
301
|
],
|
|
277
302
|
// Not in the company bundle (founder, 2026-09-24: one profile lookup, no
|
|
278
|
-
// opt-in fields)
|
|
279
|
-
|
|
303
|
+
// opt-in fields). Its own bundle since 2026-09-29, the Tech stack shape:
|
|
304
|
+
// one waterfall column over these fields; `find company` answers one company.
|
|
305
|
+
surfaces: { preset: "company_funding", find: "find company --domain <domain> --fields funding,funding_stage,latest_funding_round,total_funding,funding_rounds_count,investors", companyFields: ["funding", "funding_stage", "latest_funding_round", "total_funding", "funding_rounds_count", "investors"], managedFunction: "company_total_funding", catalogEntry: "preset:company_funding", registryFunction: "enrichment", registryIntent: "enrich_company", registryInputShapes: ["field:funding"] },
|
|
280
306
|
status: "default",
|
|
281
307
|
decision: "D49",
|
|
282
308
|
},
|
|
283
309
|
{
|
|
284
310
|
id: "enrich_company:technologies",
|
|
285
311
|
label: "Tech stack",
|
|
286
|
-
description: "Technologies the company runs: from its job postings with confidence and first/last-seen dates, or from its website
|
|
312
|
+
description: "Technologies the company runs: from its job postings with confidence and first/last-seen dates, or from its website.",
|
|
287
313
|
entity: "company",
|
|
288
314
|
group: "company",
|
|
289
315
|
kind: "enrichment",
|
|
290
316
|
rank: 150,
|
|
291
317
|
aliases: ["tech stack", "technologies they use", "technographics", "tools they use", "software they use", "built with"],
|
|
292
318
|
inputShapes: ["domain"],
|
|
293
|
-
|
|
319
|
+
// The yes/no check for named technologies is `find company --fields
|
|
320
|
+
// technology_check --check-technologies <names>`, one company at a time: no
|
|
321
|
+
// preset takes a technology list since 2026-09-24, so the column never has it.
|
|
322
|
+
outputFields: [f("company_technologies", "Technologies", "company.technologies", "jsonb")],
|
|
294
323
|
surfaces: { preset: "company_tech_stack", companyFields: ["technologies", "technology_check"], managedFunction: "company_tech_stack", catalogEntry: "preset:company_tech_stack", registryFunction: "enrichment", registryIntent: "enrich_company", registryInputShapes: ["field:technologies"] },
|
|
295
324
|
status: "default",
|
|
296
325
|
decision: "D49",
|
|
@@ -305,9 +334,9 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
|
|
|
305
334
|
rank: 160,
|
|
306
335
|
aliases: ["open roles", "job openings", "are they hiring", "is hiring", "job postings", "hiring signals"],
|
|
307
336
|
inputShapes: ["domain", "linkedin_url"],
|
|
308
|
-
outputFields: [f("company_hiring_signals", "Hiring signals", null, "jsonb"), f("company_job_openings", "Open roles", null, "
|
|
309
|
-
// Not in the company bundle (founder, 2026-09-24).
|
|
310
|
-
surfaces: { find: "find company --domain <domain> --fields hiring_signals,job_openings", companyFields: ["hiring_signals", "job_openings"], managedFunction: "company_job_openings", registryFunction: "enrichment", registryIntent: "enrich_company", registryInputShapes: ["field:hiring_signals"] },
|
|
337
|
+
outputFields: [f("company_hiring_signals", "Hiring signals", null, "jsonb"), f("company_job_openings", "Open roles", null, "jsonb", true)],
|
|
338
|
+
// Not in the company bundle (founder, 2026-09-24); its own bundle since 2026-09-29.
|
|
339
|
+
surfaces: { preset: "company_hiring", find: "find company --domain <domain> --fields hiring_signals,job_openings", companyFields: ["hiring_signals", "job_openings"], managedFunction: "company_job_openings", catalogEntry: "preset:company_hiring", registryFunction: "enrichment", registryIntent: "enrich_company", registryInputShapes: ["field:hiring_signals"] },
|
|
311
340
|
status: "default",
|
|
312
341
|
},
|
|
313
342
|
{
|
|
@@ -432,8 +461,8 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
|
|
|
432
461
|
aliases: ["revenue", "annual revenue", "revenue estimate", "how much do they make", "arr", "reported revenue", "exact revenue", "financials", "company revenue"],
|
|
433
462
|
inputShapes: ["domain"],
|
|
434
463
|
outputFields: [f("company_revenue", "Revenue (estimate)", "company.revenue")],
|
|
435
|
-
// Not in the company bundle (founder, 2026-09-24).
|
|
436
|
-
surfaces: { find: "find company --domain <domain> --fields revenue", companyFields: ["revenue"], managedFunction: "company_revenue_estimate", registryFunction: "enrichment", registryIntent: "enrich_company", registryInputShapes: ["field:revenue"] },
|
|
464
|
+
// Not in the company bundle (founder, 2026-09-24); its own bundle since 2026-09-29.
|
|
465
|
+
surfaces: { preset: "company_revenue", find: "find company --domain <domain> --fields revenue", companyFields: ["revenue"], managedFunction: "company_revenue_estimate", catalogEntry: "preset:company_revenue", registryFunction: "enrichment", registryIntent: "enrich_company", registryInputShapes: ["field:revenue"] },
|
|
437
466
|
status: "default",
|
|
438
467
|
decision: "D50",
|
|
439
468
|
},
|
|
@@ -498,7 +527,13 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
|
|
|
498
527
|
rank: 300,
|
|
499
528
|
aliases: ["decision maker", "decision makers", "head of sales at", "people at these companies", "contacts at each account", "who should i contact", "find people at company"],
|
|
500
529
|
inputShapes: ["company_domain", "company_linkedin_url"],
|
|
501
|
-
|
|
530
|
+
// The people table a table-sourced run creates, each row linked back to its company.
|
|
531
|
+
outputFields: [
|
|
532
|
+
f("first_name", "First name", "person.first_name"),
|
|
533
|
+
f("last_name", "Last name", "person.last_name"),
|
|
534
|
+
f("title", "Job title", "person.job_title"),
|
|
535
|
+
f("linkedin_url", "LinkedIn URL", "person.linkedin_url"),
|
|
536
|
+
],
|
|
502
537
|
surfaces: { managedFunction: "find_people_at_company", registryFunction: "people_search", registryIntent: "account_contacts" },
|
|
503
538
|
status: "default",
|
|
504
539
|
},
|
|
@@ -512,7 +547,7 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
|
|
|
512
547
|
rank: 310,
|
|
513
548
|
aliases: ["find people matching", "people search", "persona list", "prospect list", "leads matching"],
|
|
514
549
|
inputShapes: ["persona_filters"],
|
|
515
|
-
outputFields: [f("full_name", "Full name", "person.full_name"), f("
|
|
550
|
+
outputFields: [f("full_name", "Full name", "person.full_name"), f("title", "Job title", "person.job_title"), f("linkedin_url", "LinkedIn URL", "person.linkedin_url")],
|
|
516
551
|
surfaces: { registryFunction: "people_search", registryIntent: "persona_search" },
|
|
517
552
|
status: "default",
|
|
518
553
|
},
|
|
@@ -25,6 +25,13 @@ export type LinkedinUrlNormalization = {
|
|
|
25
25
|
dispatchUrl: string;
|
|
26
26
|
} | null;
|
|
27
27
|
export declare function normalizeLinkedinProfileUrl(raw: string | null | undefined): LinkedinUrlNormalization;
|
|
28
|
+
/**
|
|
29
|
+
* True when a profile handle is a member PROVIDER id (`ACoAA…` / `ACwAA…`), not a
|
|
30
|
+
* public identifier. People search answers some members with `/in/<id>` instead
|
|
31
|
+
* of their public URL; such a URL never equals the member's public profile URL
|
|
32
|
+
* from any other source, so it cannot serve as a LinkedIn URL identity key.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isLinkedinMemberIdHandle(handle: string | null | undefined): boolean;
|
|
28
35
|
/**
|
|
29
36
|
* Canonicalize any LinkedIn PROFILE REFERENCE a customer might put in a cell:
|
|
30
37
|
* a full `/in/` URL, a scheme-less `linkedin.com/in/…`, or a BARE public handle
|
|
@@ -75,6 +75,15 @@ export function normalizeLinkedinProfileUrl(raw) {
|
|
|
75
75
|
// too (`ACOAA…`, the shape a spreadsheet UPPER() or a CRM export produces), so
|
|
76
76
|
// such an id is named as unrecognized at enrollment instead of dying at send.
|
|
77
77
|
const LINKEDIN_MEMBER_ID_PREFIX_RE = /^AC[OoWw]/;
|
|
78
|
+
/**
|
|
79
|
+
* True when a profile handle is a member PROVIDER id (`ACoAA…` / `ACwAA…`), not a
|
|
80
|
+
* public identifier. People search answers some members with `/in/<id>` instead
|
|
81
|
+
* of their public URL; such a URL never equals the member's public profile URL
|
|
82
|
+
* from any other source, so it cannot serve as a LinkedIn URL identity key.
|
|
83
|
+
*/
|
|
84
|
+
export function isLinkedinMemberIdHandle(handle) {
|
|
85
|
+
return typeof handle === "string" && LINKEDIN_MEMBER_ID_PREFIX_RE.test(handle.trim());
|
|
86
|
+
}
|
|
78
87
|
// LinkedIn's public identifier ("vanity slug") charset: letters, digits and
|
|
79
88
|
// hyphens only, 3–100 characters, never leading with a hyphen. Deliberately
|
|
80
89
|
// NARROWER than "any non-URL string" — no `.`, `@`, `_`, `:`, `/` or whitespace
|
|
@@ -18,6 +18,7 @@ export declare function withLogContext<T>(ctx: LogContext, fn: () => T): T;
|
|
|
18
18
|
export declare function enterLogContext(ctx: LogContext): void;
|
|
19
19
|
export declare function logLevelForHttpStatus(status: number): LogLevel;
|
|
20
20
|
export type LogSink = (record: Record<string, unknown>) => void;
|
|
21
|
+
export declare function takeWebLogVolumeCount(): number;
|
|
21
22
|
export declare function setLogSink(sink: LogSink | null): void;
|
|
22
23
|
export declare function getLogSink(): LogSink | null;
|
|
23
24
|
export declare function log(level: LogLevel, msg: string, fields?: Record<string, unknown>): void;
|
|
@@ -115,6 +115,17 @@ export function logLevelForHttpStatus(status) {
|
|
|
115
115
|
// object all bundle copies share; Symbol.for gives every copy the same slot.
|
|
116
116
|
// Same trap + same fix as linkedin-quota's enforcer registration.
|
|
117
117
|
const LOG_SINK_SLOT = Symbol.for("oxygen.log.sink");
|
|
118
|
+
const WEB_LOG_VOLUME_SLOT = Symbol.for("oxygen.log.web-volume");
|
|
119
|
+
// Count the same web records eligible for the log sink before the OTLP WARN
|
|
120
|
+
// floor discards INFO. The slot must be shared across Next.js route bundles and
|
|
121
|
+
// instrumentation, just like LOG_SINK_SLOT. A one-minute summary lets SigNoz
|
|
122
|
+
// detect a fall in total logging without storing every INFO record.
|
|
123
|
+
export function takeWebLogVolumeCount() {
|
|
124
|
+
const carrier = globalThis;
|
|
125
|
+
const count = carrier[WEB_LOG_VOLUME_SLOT] ?? 0;
|
|
126
|
+
carrier[WEB_LOG_VOLUME_SLOT] = 0;
|
|
127
|
+
return count;
|
|
128
|
+
}
|
|
118
129
|
export function setLogSink(sink) {
|
|
119
130
|
globalThis[LOG_SINK_SLOT] = sink;
|
|
120
131
|
}
|
|
@@ -188,6 +199,10 @@ export function log(level, msg, fields) {
|
|
|
188
199
|
const logSink = getLogSink();
|
|
189
200
|
if (!logSink)
|
|
190
201
|
return;
|
|
202
|
+
if (record.service_name === "oxygen-web" && msg !== "web.log_volume") {
|
|
203
|
+
const carrier = globalThis;
|
|
204
|
+
carrier[WEB_LOG_VOLUME_SLOT] = (carrier[WEB_LOG_VOLUME_SLOT] ?? 0) + 1;
|
|
205
|
+
}
|
|
191
206
|
try {
|
|
192
207
|
logSink(record);
|
|
193
208
|
}
|
|
@@ -2,6 +2,7 @@ type AttributeContainer = {
|
|
|
2
2
|
attributes?: Record<string, unknown>;
|
|
3
3
|
name?: string;
|
|
4
4
|
};
|
|
5
|
+
export declare function maskEmails(value: string): string;
|
|
5
6
|
/** Sink-local copy; the managed logger retains its original record. */
|
|
6
7
|
export declare function operationalLogSnapshot(record: Record<string, unknown>): Record<string, unknown>;
|
|
7
8
|
/** Export a sanitized copy; never mutate the span another destination reads. */
|
|
@@ -3,18 +3,22 @@ import { forwardExporterMethods } from "./telemetry-export-observer.js";
|
|
|
3
3
|
// Namespaced semantic-convention fields evade the generic exact-key log policy.
|
|
4
4
|
// Numeric usage remains operational data, subject to the shared secret policy.
|
|
5
5
|
const PAYLOAD_KEY = /(?:^|[._-])(?:prompts?|completions?|inputs?|outputs?|payloads?|rows?|messages?|content|body|customer_data)(?:$|[._-])|^(?:request|response)$/i;
|
|
6
|
+
// Error text, not a message body: ADR 0023 logs the detail withheld from a
|
|
7
|
+
// customer as `error_message` (errorFields) or `<scope>_error_message`, and
|
|
8
|
+
// SigNoz is the only dev log store. Sanitized like `error` (decided 2026-09-29).
|
|
9
|
+
const ERROR_TEXT_KEY = /(?:^|_)error_message$/i;
|
|
6
10
|
const EXCEPTION_CONTENT = /^exception\.(?:message|stacktrace)$/i;
|
|
7
11
|
const QUERY_CONTENT = /^(?:db\.(?:statement|query\.text)(?:[._].*)?|(?:http|url)\.(?:query|query_string|request\.query)|process\.command(?:_args|_line)?)$/i;
|
|
8
12
|
// Addresses keep their domain (the operational signal) and drop the person.
|
|
9
13
|
const EMAIL_ADDRESS = /[A-Za-z0-9._%+-]+@((?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,})/g;
|
|
10
|
-
function maskEmails(value) {
|
|
14
|
+
export function maskEmails(value) {
|
|
11
15
|
return value.replace(EMAIL_ADDRESS, "***@$1");
|
|
12
16
|
}
|
|
13
17
|
function operationalText(value) {
|
|
14
18
|
return maskEmails(redactSecretsInString(value).replace(/(?:https?:\/\/|\/)[^\s"'<>]+/gi, stripOperationalUrl));
|
|
15
19
|
}
|
|
16
20
|
function operationalValue(value, key, depth = 0) {
|
|
17
|
-
if (PAYLOAD_KEY.test(key) && typeof value !== "number" && typeof value !== "boolean")
|
|
21
|
+
if (PAYLOAD_KEY.test(key) && !ERROR_TEXT_KEY.test(key) && typeof value !== "number" && typeof value !== "boolean")
|
|
18
22
|
return "[omitted]";
|
|
19
23
|
if (typeof value === "string")
|
|
20
24
|
return /(?:^|[._-])(?:url|path|target|href)(?:$|[._-])/i.test(key)
|
|
@@ -46,13 +46,17 @@ export type OtlpLogSinkOptions = {
|
|
|
46
46
|
/** Clock for the report throttle; tests only. */
|
|
47
47
|
now?: (() => number) | undefined;
|
|
48
48
|
/**
|
|
49
|
-
* Lowest level this sink ships
|
|
50
|
-
* or absent ships every level. Sinks
|
|
51
|
-
* stdout are unaffected
|
|
49
|
+
* Lowest level this sink ships, except exact messages listed in
|
|
50
|
+
* includeMessagesBelowMinLevel. Null or absent ships every level. Sinks
|
|
51
|
+
* composed beside this one (Axiom) and stdout are unaffected.
|
|
52
52
|
*/
|
|
53
53
|
minLevel?: LogLevel | null | undefined;
|
|
54
|
+
/** Exact, low-volume monitor events that must reach OTLP below minLevel. */
|
|
55
|
+
includeMessagesBelowMinLevel?: readonly string[] | undefined;
|
|
54
56
|
fetch?: typeof globalThis.fetch | undefined;
|
|
55
57
|
};
|
|
58
|
+
/** Source alerts #13, #16, #17, #19, #43, #45, #51, #71, #73, #75–77, #87, #90, #92, #95 and #97 need these INFO events. */
|
|
59
|
+
export declare const OTLP_MONITOR_INFO_EVENTS: readonly ["tenant.migrations.cron.done", "outbound_send.sent", "worker_workflow_cron_overlap_skipped", "worker_workflow_cron_enqueued", "copilot.turn.finished", "worker.queue.heartbeat", "worker.queue.ledger", "worker.queue.redis", "worker.queue.unregistered_kind", "automation_usage.reconcile_completed", "provider_health.cron_completed", "provider_balances.cron_completed", "cost_snapshot.cron_completed", "worker.egress_order_reconcile_sweep", "posthog_canary.round_trip_ok", "web.log_volume"];
|
|
56
60
|
export type OtlpLogSinkHandle = {
|
|
57
61
|
sink: LogSink;
|
|
58
62
|
flush: () => Promise<void>;
|
|
@@ -6,6 +6,25 @@ import { sanitizeLogFields } from "./redaction.js";
|
|
|
6
6
|
import { trace } from "@opentelemetry/api";
|
|
7
7
|
import { operationalLogSnapshot } from "./operational-telemetry.js";
|
|
8
8
|
import { telemetryEndpointForLog } from "./telemetry-export-observer.js";
|
|
9
|
+
/** Source alerts #13, #16, #17, #19, #43, #45, #51, #71, #73, #75–77, #87, #90, #92, #95 and #97 need these INFO events. */
|
|
10
|
+
export const OTLP_MONITOR_INFO_EVENTS = [
|
|
11
|
+
"tenant.migrations.cron.done",
|
|
12
|
+
"outbound_send.sent",
|
|
13
|
+
"worker_workflow_cron_overlap_skipped",
|
|
14
|
+
"worker_workflow_cron_enqueued",
|
|
15
|
+
"copilot.turn.finished",
|
|
16
|
+
"worker.queue.heartbeat",
|
|
17
|
+
"worker.queue.ledger",
|
|
18
|
+
"worker.queue.redis",
|
|
19
|
+
"worker.queue.unregistered_kind",
|
|
20
|
+
"automation_usage.reconcile_completed",
|
|
21
|
+
"provider_health.cron_completed",
|
|
22
|
+
"provider_balances.cron_completed",
|
|
23
|
+
"cost_snapshot.cron_completed",
|
|
24
|
+
"worker.egress_order_reconcile_sweep",
|
|
25
|
+
"posthog_canary.round_trip_ok",
|
|
26
|
+
"web.log_volume",
|
|
27
|
+
];
|
|
9
28
|
const DEFAULT_BATCH_SIZE = 500;
|
|
10
29
|
const DEFAULT_FLUSH_INTERVAL_MS = 2_000;
|
|
11
30
|
const DEFAULT_MAX_QUEUE = 10_000;
|
|
@@ -189,6 +208,7 @@ export function createOtlpLogSink(options) {
|
|
|
189
208
|
const doFetch = options.fetch ?? globalThis.fetch;
|
|
190
209
|
const now = options.now ?? Date.now;
|
|
191
210
|
const minSeverity = options.minLevel ? SEVERITY_NUMBERS[options.minLevel] : null;
|
|
211
|
+
const includedMessages = new Set(options.includeMessagesBelowMinLevel ?? []);
|
|
192
212
|
// A null/undefined override is dropped rather than applied, so a caller that
|
|
193
213
|
// resolves its own environment and comes back empty-handed falls back to the
|
|
194
214
|
// default here instead of shipping records with no `deployment.environment`.
|
|
@@ -275,7 +295,8 @@ export function createOtlpLogSink(options) {
|
|
|
275
295
|
if (stopped || emittingInternal)
|
|
276
296
|
return;
|
|
277
297
|
// An unknown level ships as INFO (see toLogRecord), so it filters as one.
|
|
278
|
-
if (minSeverity !== null && SEVERITY_NUMBERS[recordLevel(record)] < minSeverity
|
|
298
|
+
if (minSeverity !== null && SEVERITY_NUMBERS[recordLevel(record)] < minSeverity
|
|
299
|
+
&& (typeof record.msg !== "string" || !includedMessages.has(record.msg)))
|
|
279
300
|
return;
|
|
280
301
|
// Drop the OLDEST record and keep this one: under sustained backpressure the
|
|
281
302
|
// newest lines are the ones describing the incident being debugged.
|
|
@@ -807,21 +807,17 @@ export declare function resolvePlanAgentCreditLimits(tier: LimitsTier, options?:
|
|
|
807
807
|
*/
|
|
808
808
|
export declare const AGENT_RUN_APPROVED_CEILING_MAX_CREDITS = 25000;
|
|
809
809
|
/** Where an org's default Agent run ceiling came from. */
|
|
810
|
-
export type AgentRunDefaultCeilingSource = "plan"
|
|
810
|
+
export type AgentRunDefaultCeilingSource = "plan";
|
|
811
811
|
/** The Agent run ceilings an org's runs are held to without a further approval. */
|
|
812
812
|
export type AgentRunDefaultCeilings = PlanAgentCreditLimits & {
|
|
813
813
|
source: AgentRunDefaultCeilingSource;
|
|
814
814
|
};
|
|
815
815
|
/**
|
|
816
|
-
* The default Agent run ceilings for an org
|
|
817
|
-
*
|
|
818
|
-
* replaces the rung's total and model calls take half of it, as on every paid
|
|
819
|
-
* rung. A contract value is clamped to the platform backstop; an unusable one
|
|
820
|
-
* (absent, non-numeric, not positive) is ignored.
|
|
816
|
+
* The default Agent run ceilings for an org come from its plan rung. Enterprise
|
|
817
|
+
* uses the $1,999 rung; any higher run ceiling needs explicit approval.
|
|
821
818
|
*/
|
|
822
819
|
export declare function resolveAgentRunDefaultCeilings(input: {
|
|
823
820
|
tier: LimitsTier;
|
|
824
|
-
enterpriseDefaultCredits?: number | null;
|
|
825
821
|
options?: RepricingOptions;
|
|
826
822
|
}): AgentRunDefaultCeilings;
|
|
827
823
|
/** `details.approval_kind` on the refusal below, so every surface can render its own approval card. */
|
|
@@ -368,22 +368,10 @@ export function resolvePlanAgentCreditLimits(tier, options = {}) {
|
|
|
368
368
|
*/
|
|
369
369
|
export const AGENT_RUN_APPROVED_CEILING_MAX_CREDITS = 25_000;
|
|
370
370
|
/**
|
|
371
|
-
* The default Agent run ceilings for an org
|
|
372
|
-
*
|
|
373
|
-
* replaces the rung's total and model calls take half of it, as on every paid
|
|
374
|
-
* rung. A contract value is clamped to the platform backstop; an unusable one
|
|
375
|
-
* (absent, non-numeric, not positive) is ignored.
|
|
371
|
+
* The default Agent run ceilings for an org come from its plan rung. Enterprise
|
|
372
|
+
* uses the $1,999 rung; any higher run ceiling needs explicit approval.
|
|
376
373
|
*/
|
|
377
374
|
export function resolveAgentRunDefaultCeilings(input) {
|
|
378
|
-
const contract = input.enterpriseDefaultCredits;
|
|
379
|
-
if (typeof contract === "number" && Number.isFinite(contract) && contract >= 1) {
|
|
380
|
-
const total = Math.min(Math.floor(contract), AGENT_RUN_APPROVED_CEILING_MAX_CREDITS);
|
|
381
|
-
return {
|
|
382
|
-
maxTotalCredits: total,
|
|
383
|
-
maxInferenceCredits: Math.max(1, Math.floor(total / 2)),
|
|
384
|
-
source: "enterprise_contract",
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
375
|
return { ...resolvePlanAgentCreditLimits(input.tier, input.options), source: "plan" };
|
|
388
376
|
}
|
|
389
377
|
/** `details.approval_kind` on the refusal below, so every surface can render its own approval card. */
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { SequenceSendWindow } from "./sequences.js";
|
|
2
|
+
/**
|
|
3
|
+
* The one sending schedule Launch shows (web editor, CLI `--schedule-*`, MCP
|
|
4
|
+
* `sending_schedule`). Email and social dispatch keep separate stored windows,
|
|
5
|
+
* `settings.email_send_window` and `settings.schedule`, so one schedule is
|
|
6
|
+
* written to both.
|
|
7
|
+
*
|
|
8
|
+
* A leaf of its own, importing only types: the Launch editor's day and time
|
|
9
|
+
* pickers are also rendered on Connections, and a value import of the whole
|
|
10
|
+
* sequences module added 13.8 KB gzip to that page (measured 2026-09-29).
|
|
11
|
+
*/
|
|
12
|
+
export type SendingSchedule = {
|
|
13
|
+
timezone: string;
|
|
14
|
+
days: number[];
|
|
15
|
+
start: string;
|
|
16
|
+
end: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The settings fragment that applies one schedule to every channel. The email
|
|
20
|
+
* window keeps its recipient-timezone fields; the social window takes the same
|
|
21
|
+
* zone, days and hours.
|
|
22
|
+
*/
|
|
23
|
+
export declare function sendingScheduleSettings(emailWindow: SequenceSendWindow | null | undefined, schedule: SendingSchedule): {
|
|
24
|
+
email_send_window: SequenceSendWindow;
|
|
25
|
+
schedule: SendingSchedule;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Stable comparison key for a window: zone, sorted ISO days and hours. Days are
|
|
29
|
+
* read the way dispatch reads them (sendWindowAllowsWeekday in ./sequences):
|
|
30
|
+
* day names count, and a missing, malformed or unrecognisable list means every
|
|
31
|
+
* day.
|
|
32
|
+
*/
|
|
33
|
+
export declare function sendingScheduleKey(window: Pick<SequenceSendWindow, "timezone" | "days" | "start" | "end">): string;
|
|
34
|
+
/**
|
|
35
|
+
* Whether a mixed journey's two stored windows disagree: the social window is
|
|
36
|
+
* unset, or its zone, days or hours differ from the effective email window.
|
|
37
|
+
* Launch marks this state and the launch check warns about it.
|
|
38
|
+
*/
|
|
39
|
+
export declare function sendingWindowsDiverge(effectiveEmailWindow: SequenceSendWindow, socialWindow: Pick<SequenceSendWindow, "timezone" | "days" | "start" | "end"> | null | undefined): boolean;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The settings fragment that applies one schedule to every channel. The email
|
|
3
|
+
* window keeps its recipient-timezone fields; the social window takes the same
|
|
4
|
+
* zone, days and hours.
|
|
5
|
+
*/
|
|
6
|
+
export function sendingScheduleSettings(emailWindow, schedule) {
|
|
7
|
+
return { email_send_window: { ...emailWindow, ...schedule }, schedule: { ...schedule } };
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Stable comparison key for a window: zone, sorted ISO days and hours. Days are
|
|
11
|
+
* read the way dispatch reads them (sendWindowAllowsWeekday in ./sequences):
|
|
12
|
+
* day names count, and a missing, malformed or unrecognisable list means every
|
|
13
|
+
* day.
|
|
14
|
+
*/
|
|
15
|
+
export function sendingScheduleKey(window) {
|
|
16
|
+
const listed = Array.isArray(window.days) ? window.days : [];
|
|
17
|
+
const days = [...new Set(listed.map(isoWeekdayOf).filter((day) => day !== null))].sort((a, b) => a - b);
|
|
18
|
+
return JSON.stringify({
|
|
19
|
+
timezone: window.timezone,
|
|
20
|
+
days: days.length > 0 ? days : [1, 2, 3, 4, 5, 6, 7],
|
|
21
|
+
start: window.start,
|
|
22
|
+
end: window.end,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Whether a mixed journey's two stored windows disagree: the social window is
|
|
27
|
+
* unset, or its zone, days or hours differ from the effective email window.
|
|
28
|
+
* Launch marks this state and the launch check warns about it.
|
|
29
|
+
*/
|
|
30
|
+
export function sendingWindowsDiverge(effectiveEmailWindow, socialWindow) {
|
|
31
|
+
return !socialWindow || sendingScheduleKey(socialWindow) !== sendingScheduleKey(effectiveEmailWindow);
|
|
32
|
+
}
|
|
33
|
+
const WEEKDAY_BY_NAME = { Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, Sun: 7 };
|
|
34
|
+
function isoWeekdayOf(entry) {
|
|
35
|
+
const day = typeof entry === "number"
|
|
36
|
+
? entry
|
|
37
|
+
: typeof entry === "string"
|
|
38
|
+
? WEEKDAY_BY_NAME[`${entry.trim().slice(0, 1).toUpperCase()}${entry.trim().slice(1, 3).toLowerCase()}`] ?? Number(entry)
|
|
39
|
+
: Number.NaN;
|
|
40
|
+
return Number.isInteger(day) && day >= 1 && day <= 7 ? day : null;
|
|
41
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Every lifecycle a person reads: the stored statuses plus the derived `completed`. */
|
|
2
|
+
export declare const SEQUENCE_LIFECYCLES: readonly ["draft", "active", "completed", "paused", "archived"];
|
|
3
|
+
export type SequenceLifecycle = (typeof SEQUENCE_LIFECYCLES)[number];
|
|
4
|
+
/**
|
|
5
|
+
* The lifecycle word a person reads for a Sequence.
|
|
6
|
+
*
|
|
7
|
+
* The stored status records launch intent only (draft, active, paused,
|
|
8
|
+
* archived), and nothing moves it when the last lead finishes, so a campaign
|
|
9
|
+
* that contacted everyone kept reading "Active" forever. Whether work remains
|
|
10
|
+
* is the derived operational state; `drained` means every enrolled lead has
|
|
11
|
+
* reached an end. An active Sequence in that state reads as completed.
|
|
12
|
+
*
|
|
13
|
+
* Derived on read and never stored: operational health is one derived
|
|
14
|
+
* contract, not another lifecycle (docs/primitives/sequences.md, 2026-08-26).
|
|
15
|
+
* Enrolling new leads makes the same Sequence read as active again with no
|
|
16
|
+
* transition to undo. A paused Sequence stays paused, because someone decided
|
|
17
|
+
* that and the decision still holds.
|
|
18
|
+
*/
|
|
19
|
+
export declare function sequenceDisplayStatus<Status extends string>(status: Status, operationalState: string | null | undefined): Status | "completed";
|