@signaliz/sdk 1.0.17 → 1.0.18

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.
@@ -837,7 +837,11 @@ var Campaigns = class {
837
837
  maxSupportedTargetCount: data.max_supported_target_count,
838
838
  brainContext: data.brain_context ?? {},
839
839
  learningHoldout: data.learning_holdout ?? {},
840
- providerRoute: mapProviderRoute(data)
840
+ providerRoute: mapProviderRoute(data),
841
+ effectiveSourceRouting: recordOrNull(data.effective_source_routing ?? data.source_routing),
842
+ sourcePlan: recordOrNull(data.source_plan ?? data.plan?.source_plan),
843
+ sourceRoutes: recordArray(data.source_routes ?? data.source_plan?.routes ?? data.plan?.source_plan?.routes),
844
+ sourceShards: recordArray(data.source_shards ?? data.source_plan?.shards ?? data.plan?.source_plan?.shards)
841
845
  };
842
846
  }
843
847
  async getCampaignBuildStatus(campaignBuildId) {
@@ -1029,6 +1033,9 @@ function recordOrNull(value) {
1029
1033
  function stringArray(value) {
1030
1034
  return Array.isArray(value) ? value.map((item) => String(item || "").trim()).filter(Boolean) : [];
1031
1035
  }
1036
+ function recordArray(value) {
1037
+ return Array.isArray(value) ? value.map(recordOrNull).filter((item) => Boolean(item)) : [];
1038
+ }
1032
1039
  function diagnosticMessages(value) {
1033
1040
  if (!Array.isArray(value)) return [];
1034
1041
  return value.map((item) => {
@@ -1190,7 +1197,8 @@ function buildArgs(request) {
1190
1197
  suppress_prior_campaign_ids: request.cacheReusePolicy.suppressPriorCampaignIds,
1191
1198
  suppress_prior_list_ids: request.cacheReusePolicy.suppressPriorListIds,
1192
1199
  uniqueness: request.cacheReusePolicy.uniqueness,
1193
- require_usage_metadata: request.cacheReusePolicy.requireUsageMetadata
1200
+ require_usage_metadata: request.cacheReusePolicy.requireUsageMetadata,
1201
+ verified_cache_ttl_days: request.cacheReusePolicy.verifiedCacheTtlDays
1194
1202
  };
1195
1203
  }
1196
1204
  if (request.brainPreflight) args.brain_preflight = request.brainPreflight;
@@ -1873,6 +1881,7 @@ var CampaignBuilderAgent = class {
1873
1881
  },
1874
1882
  brain_defaults: plan.buildRequest.brainDefaults,
1875
1883
  brain_preflight: plan.buildRequest.brainPreflight,
1884
+ learning_holdout: plan.buildRequest.learningHoldout,
1876
1885
  campaign_strategy_context: plan.buildRequest.campaignStrategyContext,
1877
1886
  delivery_risk_preflight: plan.buildRequest.deliveryRisk,
1878
1887
  delivery_risk: plan.buildRequest.deliveryRisk
@@ -1886,7 +1895,8 @@ var CampaignBuilderAgent = class {
1886
1895
  estimated_credits: plan.estimatedCredits
1887
1896
  },
1888
1897
  brain_delivery_risk_preflight: plan.buildRequest.deliveryRisk,
1889
- delivery_risk: plan.buildRequest.deliveryRisk
1898
+ delivery_risk: plan.buildRequest.deliveryRisk,
1899
+ learning_holdout: plan.buildRequest.learningHoldout
1890
1900
  },
1891
1901
  approval_required: plan.approvals.some((approval) => approval.blocking),
1892
1902
  actor_type: "agent",
@@ -3292,6 +3302,21 @@ function routesFromCustomerTools(tools) {
3292
3302
  }))
3293
3303
  );
3294
3304
  }
3305
+ function normalizeCampaignBuilderLearningHoldout(input, gtmCampaignId, campaignName) {
3306
+ const source = asRecord(input);
3307
+ if (source.enabled === false) return { enabled: false };
3308
+ const controlPercentage = numberOrUndefined2(source.control_percentage ?? source.controlPercentage);
3309
+ const minControlSize = numberOrUndefined2(source.min_control_size ?? source.minControlSize);
3310
+ const experimentKey = stringValue(source.experiment_key ?? source.experimentKey) ?? (gtmCampaignId ? `gtm_campaign:${gtmCampaignId}:learning_holdout` : `campaign_builder_agent:${hashString(campaignName)}:learning_holdout`);
3311
+ return {
3312
+ enabled: true,
3313
+ controlPercentage: controlPercentage ?? 0.2,
3314
+ minControlSize: minControlSize ?? 50,
3315
+ experimentKey,
3316
+ sourceCampaignId: stringValue(source.source_campaign_id ?? source.sourceCampaignId) ?? gtmCampaignId,
3317
+ primaryMetric: stringValue(source.primary_metric ?? source.primaryMetric) ?? "positive_reply_rate"
3318
+ };
3319
+ }
3295
3320
  function createBuildRequest(request, defaults, targetCount, campaignName, scopeBuildArgs, routes = [], memory, scopeBrainPreflight) {
3296
3321
  const buildRequest = {
3297
3322
  name: campaignName,
@@ -3324,6 +3349,11 @@ function createBuildRequest(request, defaults, targetCount, campaignName, scopeB
3324
3349
  };
3325
3350
  const scoped = asRecord(scopeBuildArgs);
3326
3351
  buildRequest.gtmCampaignId = request.gtmCampaignId ?? stringValue(scoped.gtm_campaign_id ?? scoped.gtmCampaignId);
3352
+ buildRequest.learningHoldout = normalizeCampaignBuilderLearningHoldout(
3353
+ request.learningHoldout ?? scoped.learning_holdout ?? scoped.learningHoldout,
3354
+ buildRequest.gtmCampaignId,
3355
+ campaignName
3356
+ );
3327
3357
  if (typeof scoped.name === "string") buildRequest.name = scoped.name;
3328
3358
  if (typeof scoped.prompt === "string") buildRequest.prompt = scoped.prompt;
3329
3359
  if (typeof scoped.target_count === "number") buildRequest.targetCount = scoped.target_count;
@@ -3398,6 +3428,7 @@ function createBrainPreflight(request, buildRequest, routes, memory) {
3398
3428
  target_icp: targetIcp,
3399
3429
  provider_chain: providerChain,
3400
3430
  lead_source: providerChain[0] ?? "signaliz",
3431
+ learning_holdout: buildRequest.learningHoldout,
3401
3432
  tool_sequence: [
3402
3433
  { tool: "gtm_brain_learning_cycle_plan", purpose: "Plan the Brain learning loop for this campaign.", arguments: learningArgs },
3403
3434
  { tool: "gtm_brain_seed_defaults", purpose: "Seed campaign defaults from workspace memory and safe aggregate patterns.", arguments: defaultsArgs },
@@ -4065,6 +4096,16 @@ function buildCampaignArgs(request) {
4065
4096
  if (request.dedupKeys) args.dedup_keys = request.dedupKeys;
4066
4097
  if (request.brainPreflight) args.brain_preflight = request.brainPreflight;
4067
4098
  if (request.brainDefaults) args.brain_defaults = request.brainDefaults;
4099
+ if (request.learningHoldout) {
4100
+ args.learning_holdout = compact({
4101
+ enabled: request.learningHoldout.enabled,
4102
+ control_percentage: request.learningHoldout.controlPercentage,
4103
+ min_control_size: request.learningHoldout.minControlSize,
4104
+ experiment_key: request.learningHoldout.experimentKey,
4105
+ source_campaign_id: request.learningHoldout.sourceCampaignId,
4106
+ primary_metric: request.learningHoldout.primaryMetric
4107
+ });
4108
+ }
4068
4109
  if (request.campaignStrategyContext) args.campaign_strategy_context = request.campaignStrategyContext;
4069
4110
  if (request.deliveryRisk) args.delivery_risk = request.deliveryRisk;
4070
4111
  if (request.icp) {
@@ -4161,6 +4202,7 @@ function buildExecutionPrepareArgs(request, dryRun, confirmSpend) {
4161
4202
  delivery_risk: buildArgs2.delivery_risk,
4162
4203
  dedup_keys: buildArgs2.dedup_keys,
4163
4204
  policy: buildArgs2.policy,
4205
+ learning_holdout: buildArgs2.learning_holdout,
4164
4206
  signals: buildArgs2.signals,
4165
4207
  qualification: buildArgs2.qualification,
4166
4208
  copy: buildArgs2.copy,
@@ -4195,10 +4237,145 @@ function mapBuildResult(data, dryRun) {
4195
4237
  targetLimitReason: data.target_limit_reason,
4196
4238
  maxSupportedTargetCount: data.max_supported_target_count,
4197
4239
  brainContext: data.brain_context ?? {},
4240
+ learningHoldout: data.learning_holdout ?? {},
4198
4241
  providerRoute: mapProviderRoute2(data)
4199
4242
  };
4200
4243
  }
4244
+ var CAMPAIGN_BUILDER_TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "canceled", "cancelled"]);
4245
+ var CAMPAIGN_BUILDER_BLOCKED_TERMINAL_STATUSES = /* @__PURE__ */ new Set(["failed", "canceled", "cancelled"]);
4246
+ function mapAgentCampaignPhaseHealth(value) {
4247
+ if (!Array.isArray(value)) return [];
4248
+ return value.map((phase) => {
4249
+ const record = asRecord(phase);
4250
+ return {
4251
+ phase: stringValue(record.phase) ?? "unknown",
4252
+ status: stringValue(record.status) ?? "unknown",
4253
+ recordsProcessed: numberOrNull2(record.records_processed ?? record.recordsProcessed),
4254
+ recordsSucceeded: numberOrNull2(record.records_succeeded ?? record.recordsSucceeded),
4255
+ recordsFailed: numberOrNull2(record.records_failed ?? record.recordsFailed),
4256
+ heartbeatAgeSeconds: numberOrNull2(record.heartbeat_age_seconds ?? record.heartbeatAgeSeconds ?? record.phase_age_seconds)
4257
+ };
4258
+ });
4259
+ }
4260
+ function deriveCampaignBuildTerminalState(input) {
4261
+ const status = input.status.toLowerCase();
4262
+ const heartbeatAgeSeconds = input.phaseAgeSeconds;
4263
+ const stale = input.staleRunningPhase === true;
4264
+ const safeWatchAction = input.triggerRunId ? `signaliz ops run-status ${input.triggerRunId} --watch` : "signaliz campaign-agent status <campaign_build_id>";
4265
+ if (CAMPAIGN_BUILDER_BLOCKED_TERMINAL_STATUSES.has(status)) {
4266
+ return {
4267
+ kind: "blocked",
4268
+ label: "Blocked terminal state",
4269
+ reviewable: true,
4270
+ terminal: true,
4271
+ stale: false,
4272
+ phase: input.phase,
4273
+ phaseStatus: input.phaseStatus,
4274
+ heartbeatAgeSeconds,
4275
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4276
+ terminalReason: input.terminalReason,
4277
+ staleReason: null,
4278
+ safeNextAction: input.nextAction || "Review errors and rerun only after the blocker is resolved."
4279
+ };
4280
+ }
4281
+ if (status === "pending_approval") {
4282
+ return {
4283
+ kind: "pending_approval",
4284
+ label: "Pending delivery approval",
4285
+ reviewable: true,
4286
+ terminal: false,
4287
+ stale: false,
4288
+ phase: input.phase,
4289
+ phaseStatus: input.phaseStatus,
4290
+ heartbeatAgeSeconds,
4291
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4292
+ terminalReason: input.terminalReason,
4293
+ staleReason: null,
4294
+ safeNextAction: input.approvalAction || input.nextAction || "Review rows and approve delivery only after the approval packet passes."
4295
+ };
4296
+ }
4297
+ if (CAMPAIGN_BUILDER_TERMINAL_STATUSES.has(status)) {
4298
+ return {
4299
+ kind: "terminal",
4300
+ label: "Terminal and reviewable",
4301
+ reviewable: true,
4302
+ terminal: true,
4303
+ stale: false,
4304
+ phase: input.phase,
4305
+ phaseStatus: input.phaseStatus,
4306
+ heartbeatAgeSeconds,
4307
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4308
+ terminalReason: input.terminalReason,
4309
+ staleReason: null,
4310
+ safeNextAction: input.nextAction || "Review rows, artifacts, scorecard, and approval locks before delivery."
4311
+ };
4312
+ }
4313
+ if (status === "running" || status === "queued" || status === "processing") {
4314
+ if (stale) {
4315
+ return {
4316
+ kind: "running_stale",
4317
+ label: "Running but stale",
4318
+ reviewable: false,
4319
+ terminal: false,
4320
+ stale: true,
4321
+ phase: input.phase,
4322
+ phaseStatus: input.phaseStatus,
4323
+ heartbeatAgeSeconds,
4324
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4325
+ terminalReason: null,
4326
+ staleReason: input.staleReason || "The active phase heartbeat is stale.",
4327
+ safeNextAction: input.nextAction || safeWatchAction
4328
+ };
4329
+ }
4330
+ return {
4331
+ kind: "running_healthy",
4332
+ label: "Running and healthy",
4333
+ reviewable: false,
4334
+ terminal: false,
4335
+ stale: false,
4336
+ phase: input.phase,
4337
+ phaseStatus: input.phaseStatus,
4338
+ heartbeatAgeSeconds,
4339
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4340
+ terminalReason: null,
4341
+ staleReason: null,
4342
+ safeNextAction: input.nextAction || safeWatchAction
4343
+ };
4344
+ }
4345
+ return {
4346
+ kind: "unknown",
4347
+ label: "Unknown build state",
4348
+ reviewable: false,
4349
+ terminal: false,
4350
+ stale,
4351
+ phase: input.phase,
4352
+ phaseStatus: input.phaseStatus,
4353
+ heartbeatAgeSeconds,
4354
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4355
+ terminalReason: input.terminalReason,
4356
+ staleReason: input.staleReason,
4357
+ safeNextAction: input.nextAction || "Read campaign build status again before making a launch or delivery decision."
4358
+ };
4359
+ }
4201
4360
  function mapAgentCampaignBuildStatus(data) {
4361
+ const customerRowCounts = mapAgentCustomerRowCounts(data.customer_row_counts);
4362
+ const phaseHealth = mapAgentCampaignPhaseHealth(data.phases);
4363
+ const terminalReason = stringValue(data.terminal_reason) ?? customerRowCounts?.terminalReason ?? null;
4364
+ const staleReason = stringValue(data.stale_reason) ?? stringValue(asRecord(data.diagnostics).stale_reason) ?? null;
4365
+ const nextPollAfterSeconds = numberOrNull2(data.next_poll_after_seconds ?? data.nextPollAfterSeconds);
4366
+ const terminalState = deriveCampaignBuildTerminalState({
4367
+ status: stringValue(data.status) ?? "unknown",
4368
+ phase: stringValue(data.current_phase) ?? null,
4369
+ phaseStatus: stringValue(data.current_phase_status) ?? null,
4370
+ staleRunningPhase: data.stale_running_phase === true,
4371
+ phaseAgeSeconds: numberOrNull2(data.phase_age_seconds),
4372
+ nextPollAfterSeconds,
4373
+ terminalReason,
4374
+ staleReason,
4375
+ nextAction: formatAgentNextAction(data.next_action),
4376
+ approvalAction: formatAgentNextAction(data.approval_action),
4377
+ triggerRunId: stringValue(data.trigger_run_id) ?? null
4378
+ });
4202
4379
  return {
4203
4380
  campaignBuildId: stringValue(data.campaign_build_id) ?? "",
4204
4381
  campaignId: stringValue(data.campaign_id) ?? null,
@@ -4216,12 +4393,18 @@ function mapAgentCampaignBuildStatus(data) {
4216
4393
  errors: diagnosticMessages2(data.errors),
4217
4394
  artifactCount: numberOrUndefined2(data.artifact_count) ?? 0,
4218
4395
  artifactDownloads: Array.isArray(data.artifact_downloads) ? data.artifact_downloads.map(mapAgentCampaignArtifactDownload) : [],
4219
- customerRowCounts: mapAgentCustomerRowCounts(data.customer_row_counts),
4396
+ customerRowCounts,
4220
4397
  phaseAttemptTotals: nullableRecord(data.phase_attempt_totals) ?? void 0,
4221
4398
  providerRoute: mapProviderRoute2(data),
4222
4399
  triggerRunId: stringValue(data.trigger_run_id) ?? null,
4223
4400
  staleRunningPhase: data.stale_running_phase === true,
4224
4401
  phaseAgeSeconds: numberOrNull2(data.phase_age_seconds),
4402
+ nextPollAfterSeconds,
4403
+ terminalReason,
4404
+ staleReason,
4405
+ safeNextAction: terminalState.safeNextAction,
4406
+ terminalState,
4407
+ phaseHealth,
4225
4408
  diagnostics: nullableRecord(data.diagnostics) ?? {},
4226
4409
  nextAction: formatAgentNextAction(data.next_action),
4227
4410
  approvalAction: formatAgentNextAction(data.approval_action),
@@ -4309,67 +4492,285 @@ function mapAgentCampaignArtifact(data) {
4309
4492
  createdAt: stringValue(data.created_at) ?? ""
4310
4493
  };
4311
4494
  }
4495
+ var NORTH_STAR_BANNED_COPY_PHRASES = [
4496
+ "i have been following",
4497
+ "i saw that",
4498
+ "would you be opposed",
4499
+ "unlock new opportunities",
4500
+ "significant success",
4501
+ "we understand",
4502
+ "hope this finds you well"
4503
+ ];
4504
+ function campaignReviewGate(id, label, status, detail) {
4505
+ return {
4506
+ id,
4507
+ label,
4508
+ status,
4509
+ passed: status !== "blocked",
4510
+ detail
4511
+ };
4512
+ }
4513
+ function northStarMetric(id, label, status, detail, value, target) {
4514
+ return {
4515
+ id,
4516
+ label,
4517
+ status,
4518
+ detail,
4519
+ ...value !== void 0 ? { value } : {},
4520
+ ...target !== void 0 ? { target } : {}
4521
+ };
4522
+ }
4523
+ function createCampaignBuilderNorthStarScorecard(status, rows, artifacts, terminalState) {
4524
+ if (!terminalState.reviewable) {
4525
+ const pendingMetric = northStarMetric(
4526
+ "terminal_state",
4527
+ "Terminal-state contract",
4528
+ terminalState.kind === "running_stale" ? "blocked" : "pending",
4529
+ terminalState.kind === "running_stale" ? `Build is stale in ${terminalState.phase || "unknown phase"}; do not grade output yet.` : `Build is ${terminalState.label.toLowerCase()}; wait for terminal or approval state before grading.`,
4530
+ terminalState.kind,
4531
+ "terminal or pending_approval"
4532
+ );
4533
+ return {
4534
+ version: "campaign-builder-north-star.v1",
4535
+ status: pendingMetric.status,
4536
+ score: null,
4537
+ moatState: "scaffolding_ready",
4538
+ metrics: [pendingMetric],
4539
+ blockers: pendingMetric.status === "blocked" ? [pendingMetric.detail] : [],
4540
+ warnings: pendingMetric.status === "pending" ? [pendingMetric.detail] : [],
4541
+ nextAction: terminalState.safeNextAction
4542
+ };
4543
+ }
4544
+ const sampledRows = rows.rows.length;
4545
+ const rowData = rows.rows.map((row) => asRecord(row.data));
4546
+ const requestedTarget = status.customerRowCounts?.requestedTarget ?? status.recordsTotal ?? rows.count;
4547
+ const finalRows = status.customerRowCounts?.deliveredRows ?? status.customerRowCounts?.qaReadyRows ?? status.customerRowCounts?.copiedRows ?? status.customerRowCounts?.acceptedRows ?? rows.count;
4548
+ const rowsWithEmail = rows.rows.filter(campaignReviewRowHasEmail).length;
4549
+ const verifiedEmailRows = rowData.filter(campaignReviewDataHasVerifiedEmail).length;
4550
+ const uniqueEmails = new Set(rowData.map(campaignReviewDataEmail).filter(Boolean)).size;
4551
+ const uniqueDomains = new Set(rowData.map(campaignReviewDataDomain).filter(Boolean)).size;
4552
+ const copyReadyRows = rows.rows.filter(campaignReviewRowHasCopy).length;
4553
+ const copyQaIssuesByRow = rowData.map(campaignReviewDataCopyQaIssues).filter((issues) => issues.length > 0);
4554
+ const copyQaIssueCounts = countCampaignReviewIssues(copyQaIssuesByRow);
4555
+ const signalBackedRows = rowData.filter(campaignReviewDataHasSignalBasis).length;
4556
+ const noSupportedSignalRows = rowData.filter(campaignReviewDataHasNoSupportedSignal).length;
4557
+ const unsupportedSignalClaims = rowData.filter(campaignReviewDataHasUnsupportedSignalClaim).length;
4558
+ const approvalLockedRows = rowData.filter(campaignReviewDataHasApprovalLock).length;
4559
+ const holdoutRows = rowData.filter(campaignReviewDataHasHoldoutAttribution).length;
4560
+ const artifactCount = artifacts.length || status.artifactCount || 0;
4561
+ const deliveredRows = status.customerRowCounts?.deliveredRows ?? 0;
4562
+ const feedbackReady = campaignReviewStatusHasFeedbackReadiness(status);
4563
+ const feedbackRequiredForApproval = terminalState.kind === "pending_approval";
4564
+ const moat = campaignReviewMoatState(status, holdoutRows);
4565
+ const metrics = [
4566
+ northStarMetric(
4567
+ "terminal_state",
4568
+ "Terminal-state contract",
4569
+ terminalState.kind === "blocked" ? "blocked" : "pass",
4570
+ terminalState.label,
4571
+ terminalState.kind,
4572
+ "terminal or pending_approval"
4573
+ ),
4574
+ northStarMetric(
4575
+ "target_fill",
4576
+ "Target fill",
4577
+ requestedTarget && finalRows < requestedTarget ? "review" : "pass",
4578
+ requestedTarget ? `${finalRows || 0}/${requestedTarget} final or reviewable row(s) are accounted for.` : "No requested target was reported; using sampled rows for review.",
4579
+ finalRows || sampledRows,
4580
+ requestedTarget || sampledRows
4581
+ ),
4582
+ northStarMetric(
4583
+ "unique_emails",
4584
+ "Unique emails",
4585
+ sampledRows === 0 ? "blocked" : uniqueEmails === rowsWithEmail ? "pass" : "blocked",
4586
+ sampledRows === 0 ? "No sampled rows were available for email uniqueness review." : `${uniqueEmails}/${rowsWithEmail} sampled email(s) are unique.`,
4587
+ uniqueEmails,
4588
+ rowsWithEmail
4589
+ ),
4590
+ northStarMetric(
4591
+ "unique_domains",
4592
+ "Unique domains",
4593
+ sampledRows === 0 ? "blocked" : uniqueDomains === sampledRows ? "pass" : "review",
4594
+ sampledRows === 0 ? "No sampled rows were available for domain uniqueness review." : `${uniqueDomains}/${sampledRows} sampled company domain(s) are unique.`,
4595
+ uniqueDomains,
4596
+ sampledRows
4597
+ ),
4598
+ northStarMetric(
4599
+ "verified_email_rows",
4600
+ "Verified email rows",
4601
+ sampledRows === 0 ? "blocked" : verifiedEmailRows === sampledRows ? "pass" : rowsWithEmail === sampledRows ? "review" : "blocked",
4602
+ sampledRows === 0 ? "No sampled rows were available for email verification review." : `${verifiedEmailRows}/${sampledRows} sampled row(s) carry verified-email evidence.`,
4603
+ verifiedEmailRows,
4604
+ sampledRows
4605
+ ),
4606
+ northStarMetric(
4607
+ "copy_completeness",
4608
+ "Copy completeness",
4609
+ sampledRows === 0 ? "blocked" : copyReadyRows === sampledRows ? "pass" : "blocked",
4610
+ sampledRows === 0 ? "No sampled rows were available for copy review." : `${copyReadyRows}/${sampledRows} sampled row(s) include generated copy.`,
4611
+ copyReadyRows,
4612
+ sampledRows
4613
+ ),
4614
+ northStarMetric(
4615
+ "copy_quality",
4616
+ "Copy QA",
4617
+ copyQaIssuesByRow.length === 0 ? "pass" : "blocked",
4618
+ copyQaIssuesByRow.length === 0 ? "No sampled copy QA blockers were detected." : `${copyQaIssuesByRow.length}/${sampledRows} sampled row(s) have copy QA issue(s): ${formatCampaignReviewIssueCounts(copyQaIssueCounts)}.`,
4619
+ copyQaIssuesByRow.length,
4620
+ 0
4621
+ ),
4622
+ northStarMetric(
4623
+ "signal_grounding",
4624
+ "Signal grounding",
4625
+ unsupportedSignalClaims > 0 ? "blocked" : signalBackedRows > 0 ? "pass" : "review",
4626
+ unsupportedSignalClaims > 0 ? `${unsupportedSignalClaims} sampled row(s) make unsupported signal claims.` : signalBackedRows > 0 ? `${signalBackedRows}/${sampledRows} sampled row(s) include supported signal or personalization basis.` : noSupportedSignalRows > 0 ? `${noSupportedSignalRows}/${sampledRows} sampled row(s) are explicitly marked no_supported_signal; copy should stay conservative.` : "No sampled rows include supported signal evidence; copy should stay conservative.",
4627
+ signalBackedRows,
4628
+ sampledRows
4629
+ ),
4630
+ northStarMetric(
4631
+ "approval_locks",
4632
+ "Approval locks",
4633
+ approvalLockedRows > 0 || deliveredRows === 0 ? "pass" : "review",
4634
+ approvalLockedRows > 0 ? `${approvalLockedRows}/${sampledRows} sampled row(s) show approval/export lock metadata.` : "Approval-lock metadata was not visible in sampled rows; preserve delivery approval gates.",
4635
+ approvalLockedRows,
4636
+ sampledRows
4637
+ ),
4638
+ northStarMetric(
4639
+ "export_send_safety",
4640
+ "Export/send safety",
4641
+ deliveredRows > 0 && status.status !== "completed" ? "blocked" : "pass",
4642
+ deliveredRows > 0 ? `${deliveredRows} row(s) are delivered; status is ${status.status}.` : "No delivered rows are reported before approval.",
4643
+ deliveredRows,
4644
+ 0
4645
+ ),
4646
+ northStarMetric(
4647
+ "holdout_attribution",
4648
+ "Holdout attribution",
4649
+ holdoutRows > 0 ? "pass" : "review",
4650
+ holdoutRows > 0 ? `${holdoutRows}/${sampledRows} sampled row(s) preserve holdout attribution.` : "No holdout attribution was visible in the sampled rows.",
4651
+ holdoutRows,
4652
+ sampledRows
4653
+ ),
4654
+ northStarMetric(
4655
+ "feedback_readiness",
4656
+ "Feedback readiness",
4657
+ feedbackReady ? "pass" : feedbackRequiredForApproval ? "blocked" : "review",
4658
+ feedbackReady ? "Feedback readiness is visible in status diagnostics." : feedbackRequiredForApproval ? "Feedback webhook or approved pull readiness is required before delivery approval." : "Feedback webhook or approved pull readiness was not visible; require it before real send approval.",
4659
+ feedbackReady,
4660
+ true
4661
+ ),
4662
+ northStarMetric(
4663
+ "causal_moat",
4664
+ "Causal moat",
4665
+ moat.state === "proved" ? "pass" : "review",
4666
+ moat.detail,
4667
+ moat.state,
4668
+ "proved"
4669
+ ),
4670
+ northStarMetric(
4671
+ "artifact_readback",
4672
+ "Artifact readback",
4673
+ artifactCount > 0 ? "pass" : "review",
4674
+ artifactCount > 0 ? `${artifactCount} artifact(s) are available for review.` : "No review artifact is available yet.",
4675
+ artifactCount,
4676
+ 1
4677
+ )
4678
+ ];
4679
+ const gradedMetrics = metrics.filter((metric) => metric.status !== "pending");
4680
+ const score = gradedMetrics.length === 0 ? null : Math.round(gradedMetrics.filter((metric) => metric.status === "pass").length / gradedMetrics.length * 100);
4681
+ const blockers = metrics.filter((metric) => metric.status === "blocked").map((metric) => metric.detail);
4682
+ const warnings = metrics.filter((metric) => metric.status === "review").map((metric) => metric.detail);
4683
+ const scorecardStatus = blockers.length > 0 ? "blocked" : warnings.length > 0 ? "review" : "pass";
4684
+ return {
4685
+ version: "campaign-builder-north-star.v1",
4686
+ status: scorecardStatus,
4687
+ score,
4688
+ moatState: moat.state,
4689
+ metrics,
4690
+ blockers,
4691
+ warnings,
4692
+ nextAction: scorecardStatus === "blocked" ? "Fix blocked North Star gates before approval or delivery." : terminalState.safeNextAction
4693
+ };
4694
+ }
4312
4695
  function createCampaignBuilderBuildReview(status, rows, artifacts) {
4696
+ const terminalState = status.terminalState ?? deriveCampaignBuildTerminalState({
4697
+ status: status.status,
4698
+ phase: status.currentPhase,
4699
+ phaseStatus: status.currentPhaseStatus,
4700
+ staleRunningPhase: status.staleRunningPhase === true,
4701
+ phaseAgeSeconds: status.phaseAgeSeconds ?? null,
4702
+ nextPollAfterSeconds: status.nextPollAfterSeconds ?? null,
4703
+ terminalReason: status.terminalReason ?? status.customerRowCounts?.terminalReason ?? null,
4704
+ staleReason: status.staleReason ?? null,
4705
+ nextAction: status.nextAction,
4706
+ approvalAction: status.approvalAction,
4707
+ triggerRunId: status.triggerRunId ?? null
4708
+ });
4313
4709
  const sampledRows = rows.rows.length;
4314
4710
  const availableRows = rows.count || sampledRows;
4315
4711
  const qualifiedRows = rows.rows.filter((row) => row.qualified !== false).length;
4316
4712
  const rowsWithEmail = rows.rows.filter(campaignReviewRowHasEmail).length;
4317
4713
  const copyReadyRows = rows.rows.filter(campaignReviewRowHasCopy).length;
4318
4714
  const artifactCount = artifacts.length || status.artifactCount || 0;
4715
+ const northStarScorecard = createCampaignBuilderNorthStarScorecard(status, rows, artifacts, terminalState);
4319
4716
  const gates = [
4320
- {
4321
- id: "build_completed",
4322
- label: "Build completed",
4323
- passed: status.status === "completed",
4324
- detail: status.status === "completed" ? "The campaign build is completed." : `The campaign build is ${status.status}; review again after completion.`
4325
- },
4326
- {
4327
- id: "rows_available",
4328
- label: "Rows available",
4329
- passed: sampledRows > 0,
4330
- detail: sampledRows > 0 ? `${sampledRows} sampled row(s) are available for review.` : "No rows were returned for review."
4331
- },
4332
- {
4333
- id: "qualified_sample",
4334
- label: "Sample rows qualified",
4335
- passed: sampledRows > 0 && qualifiedRows === sampledRows,
4336
- detail: sampledRows > 0 ? `${qualifiedRows}/${sampledRows} sampled row(s) are marked qualified.` : "Qualification could not be checked without sampled rows."
4337
- },
4338
- {
4339
- id: "email_coverage",
4340
- label: "Email coverage",
4341
- passed: sampledRows > 0 && rowsWithEmail === sampledRows,
4342
- detail: sampledRows > 0 ? `${rowsWithEmail}/${sampledRows} sampled row(s) include an email.` : "Email coverage could not be checked without sampled rows."
4343
- },
4344
- {
4345
- id: "artifact_available",
4346
- label: "Artifact available",
4347
- passed: artifactCount > 0,
4348
- detail: artifactCount > 0 ? `${artifactCount} artifact(s) are available.` : "No delivery artifact is available yet."
4349
- },
4350
- {
4351
- id: "no_failed_rows",
4352
- label: "No failed rows",
4353
- passed: status.recordsFailed === 0,
4354
- detail: status.recordsFailed === 0 ? "The build reports zero failed rows." : `${status.recordsFailed} row(s) failed during the build.`
4355
- }
4717
+ campaignReviewGate(
4718
+ "build_reviewable",
4719
+ "Build reviewable",
4720
+ terminalState.kind === "running_healthy" ? "pending" : terminalState.kind === "running_stale" || terminalState.kind === "blocked" ? "blocked" : "pass",
4721
+ terminalState.label
4722
+ ),
4723
+ campaignReviewGate(
4724
+ "rows_available",
4725
+ "Rows available",
4726
+ sampledRows > 0 ? "pass" : terminalState.reviewable ? "blocked" : "pending",
4727
+ sampledRows > 0 ? `${sampledRows} sampled row(s) are available for review.` : terminalState.reviewable ? "No rows were returned for review." : "Rows are not graded until the build is reviewable."
4728
+ ),
4729
+ campaignReviewGate(
4730
+ "qualified_sample",
4731
+ "Sample rows qualified",
4732
+ sampledRows === 0 ? terminalState.reviewable ? "blocked" : "pending" : qualifiedRows === sampledRows ? "pass" : "blocked",
4733
+ sampledRows > 0 ? `${qualifiedRows}/${sampledRows} sampled row(s) are marked qualified.` : "Qualification could not be checked without sampled rows."
4734
+ ),
4735
+ campaignReviewGate(
4736
+ "email_coverage",
4737
+ "Email coverage",
4738
+ sampledRows === 0 ? terminalState.reviewable ? "blocked" : "pending" : rowsWithEmail === sampledRows ? "pass" : "blocked",
4739
+ sampledRows > 0 ? `${rowsWithEmail}/${sampledRows} sampled row(s) include an email.` : "Email coverage could not be checked without sampled rows."
4740
+ ),
4741
+ campaignReviewGate(
4742
+ "artifact_available",
4743
+ "Artifact available",
4744
+ artifactCount > 0 ? "pass" : terminalState.reviewable ? "review" : "pending",
4745
+ artifactCount > 0 ? `${artifactCount} artifact(s) are available.` : "No delivery artifact is available yet."
4746
+ ),
4747
+ campaignReviewGate(
4748
+ "no_failed_rows",
4749
+ "No failed rows",
4750
+ status.recordsFailed === 0 ? "pass" : "blocked",
4751
+ status.recordsFailed === 0 ? "The build reports zero failed rows." : `${status.recordsFailed} row(s) failed during the build.`
4752
+ )
4356
4753
  ];
4357
- const blockedReasons = gates.filter((gate) => !gate.passed).map((gate) => gate.detail);
4754
+ const blockedReasons = gates.filter((gate) => gate.status === "blocked" || !gate.passed).map((gate) => gate.detail);
4358
4755
  const warnings = [
4359
4756
  rows.hasMore ? "This review is based on a row sample; page through remaining rows before final approval." : void 0,
4360
4757
  sampledRows > 0 && copyReadyRows < sampledRows ? `${copyReadyRows}/${sampledRows} sampled row(s) include generated copy.` : void 0,
4361
4758
  ...status.warnings
4362
4759
  ].filter((item) => Boolean(item));
4363
- const readyForDeliveryApproval = blockedReasons.length === 0;
4760
+ const readyForDeliveryApproval = terminalState.reviewable && blockedReasons.length === 0 && northStarScorecard.status !== "blocked";
4364
4761
  return {
4365
4762
  campaignBuildId: status.campaignBuildId || rows.campaignBuildId,
4366
4763
  status,
4367
4764
  rows,
4368
4765
  artifacts,
4369
4766
  gates,
4767
+ northStarScorecard,
4370
4768
  summary: {
4371
4769
  status: status.status,
4372
4770
  phase: status.currentPhase,
4771
+ terminalState: terminalState.kind,
4772
+ scorecardStatus: northStarScorecard.status,
4773
+ scorecardScore: northStarScorecard.score,
4373
4774
  sampledRows,
4374
4775
  availableRows,
4375
4776
  qualifiedRows,
@@ -4423,6 +4824,184 @@ function campaignReviewRowHasCopy(row) {
4423
4824
  stringValue(data.subject) || stringValue(data.subject_line) || stringValue(data.subjectLine) || stringValue(data.opener) || stringValue(data.body) || stringValue(data.cta) || stringValue(copy.subject) || stringValue(copy.body) || stringValue(rawCopy.subject) || stringValue(rawCopy.body)
4424
4825
  );
4425
4826
  }
4827
+ function campaignReviewDataEmail(data) {
4828
+ return stringValue(data.email) || stringValue(data.work_email) || stringValue(data.workEmail) || stringValue(asRecord(data.contact).email);
4829
+ }
4830
+ function campaignReviewDataDomain(data) {
4831
+ const domain = stringValue(data.company_domain) || stringValue(data.companyDomain) || stringValue(asRecord(data.company).domain);
4832
+ return domain?.toLowerCase();
4833
+ }
4834
+ function campaignReviewDataHasVerifiedEmail(data) {
4835
+ const status = String(data.email_status ?? data.emailStatus ?? asRecord(data.contact).email_status ?? "").toLowerCase();
4836
+ return data.email_verified === true || data.emailVerified === true || asRecord(data.contact).email_verified === true || ["valid", "validated", "verified"].includes(status);
4837
+ }
4838
+ function campaignReviewDataCopyParts(data) {
4839
+ const copy = asRecord(data.copy);
4840
+ const rawCopy = asRecord(data.raw_copy ?? data.rawCopy);
4841
+ return [
4842
+ stringValue(data.subject),
4843
+ stringValue(data.subject_line),
4844
+ stringValue(data.opener),
4845
+ stringValue(data.body),
4846
+ stringValue(data.cta),
4847
+ stringValue(copy.subject),
4848
+ stringValue(copy.opener),
4849
+ stringValue(copy.body),
4850
+ stringValue(copy.cta),
4851
+ stringValue(rawCopy.subject),
4852
+ stringValue(rawCopy.opener),
4853
+ stringValue(rawCopy.body),
4854
+ stringValue(rawCopy.cta)
4855
+ ].filter((part) => Boolean(part));
4856
+ }
4857
+ function campaignReviewDataHasBadGreeting(data) {
4858
+ return campaignReviewDataCopyParts(data).some(
4859
+ (part) => /\bhi\s*(?:,|\{\{|\[)|\bhello\s*(?:,|\{\{|\[)/i.test(part) || /\b(first_name|firstname|name)\b/i.test(part)
4860
+ );
4861
+ }
4862
+ function campaignReviewDataHasBannedPhrase(data) {
4863
+ const text = campaignReviewDataCopyParts(data).join(" ").toLowerCase();
4864
+ return NORTH_STAR_BANNED_COPY_PHRASES.some((phrase) => text.includes(phrase));
4865
+ }
4866
+ function campaignReviewDataSubject(data) {
4867
+ const copy = asRecord(data.copy);
4868
+ const rawCopy = asRecord(data.raw_copy ?? data.rawCopy);
4869
+ return stringValue(data.subject) || stringValue(data.subject_line) || stringValue(data.subjectLine) || stringValue(copy.subject) || stringValue(rawCopy.subject);
4870
+ }
4871
+ function campaignReviewDataCta(data) {
4872
+ const copy = asRecord(data.copy);
4873
+ const rawCopy = asRecord(data.raw_copy ?? data.rawCopy);
4874
+ return stringValue(data.cta) || stringValue(copy.cta) || stringValue(rawCopy.cta);
4875
+ }
4876
+ function countCampaignReviewIssues(issueRows) {
4877
+ return issueRows.flat().reduce((acc, issue) => {
4878
+ acc[issue] = (acc[issue] || 0) + 1;
4879
+ return acc;
4880
+ }, {});
4881
+ }
4882
+ function formatCampaignReviewIssueCounts(counts) {
4883
+ const entries = Object.entries(counts);
4884
+ return entries.length > 0 ? entries.map(([issue, count]) => `${issue}=${count}`).join(", ") : "none";
4885
+ }
4886
+ function campaignReviewDataCopyQaIssues(data) {
4887
+ const issues = /* @__PURE__ */ new Set();
4888
+ const copyParts = campaignReviewDataCopyParts(data);
4889
+ if (copyParts.length === 0) return [];
4890
+ if (campaignReviewDataHasBadGreeting(data)) issues.add("bad_greeting");
4891
+ if (campaignReviewDataHasBannedPhrase(data)) issues.add("banned_or_vague_phrase");
4892
+ if (campaignReviewDataHasUnsupportedSignalClaim(data)) issues.add("unsupported_signal_claim");
4893
+ if (campaignReviewDataPersonalizationBasis(data).length === 0) issues.add("missing_personalization_basis");
4894
+ if (!campaignReviewDataCta(data)) issues.add("missing_cta");
4895
+ const subject = campaignReviewDataSubject(data);
4896
+ if (subject && subject.length > 70) issues.add("overlong_subject");
4897
+ return [...issues];
4898
+ }
4899
+ function campaignReviewDataPersonalizationBasis(data) {
4900
+ const rawCopy = asRecord(data.raw_copy ?? data.rawCopy);
4901
+ const copy = asRecord(data.copy);
4902
+ const metadata = asRecord(data.metadata);
4903
+ const signalEvidence = asRecord(data.signal_evidence ?? metadata.signal_evidence ?? metadata.signalEvidence);
4904
+ return [
4905
+ ...arrayOfStrings(data.personalization_basis) ?? [],
4906
+ ...arrayOfStrings(data.personalizationBasis) ?? [],
4907
+ ...arrayOfStrings(copy.personalization_basis) ?? [],
4908
+ ...arrayOfStrings(rawCopy.personalization_basis) ?? [],
4909
+ ...arrayOfStrings(metadata.personalization_basis) ?? [],
4910
+ ...arrayOfStrings(metadata.personalizationBasis) ?? [],
4911
+ ...arrayOfStrings(signalEvidence.personalization_basis) ?? []
4912
+ ];
4913
+ }
4914
+ function campaignReviewDataHasNoSupportedSignal(data) {
4915
+ const metadata = asRecord(data.metadata);
4916
+ const copy = asRecord(data.copy);
4917
+ const state = [
4918
+ data.copy_signal_state,
4919
+ data.signal_basis,
4920
+ data.signal_type,
4921
+ metadata.copy_signal_state,
4922
+ metadata.signal_basis,
4923
+ metadata.signal_type,
4924
+ copy.copy_signal_state
4925
+ ].map((value) => stringValue(value)?.toLowerCase()).filter(Boolean);
4926
+ return data.no_supported_signal === true || metadata.no_supported_signal === true || state.includes("no_supported_signal");
4927
+ }
4928
+ function campaignReviewDataHasSignalBasis(data) {
4929
+ if (campaignReviewDataHasNoSupportedSignal(data)) return false;
4930
+ const metadata = asRecord(data.metadata);
4931
+ const signal = asRecord(data.signal);
4932
+ const signalEvidence = asRecord(data.signal_evidence ?? metadata.signal_evidence ?? metadata.signalEvidence);
4933
+ const basis = campaignReviewDataPersonalizationBasis(data);
4934
+ return Boolean(
4935
+ stringValue(data.signal_type) || stringValue(data.signalType) || stringValue(data.signal_summary) || stringValue(data.signalSummary) || stringValue(data.signal_source_url) || stringValue(data.signalSourceUrl) || stringValue(metadata.signal_type) || stringValue(metadata.signal_summary) || stringValue(metadata.signal_source_url) || stringValue(signal.type) || stringValue(signal.summary) || stringValue(signal.source_url) || stringValue(signalEvidence.signal_type) || stringValue(signalEvidence.summary) || stringValue(signalEvidence.source_url) || basis.some((item) => /\bsignal|hiring|funding|leadership|growth|launch|news|technology\b/i.test(item))
4936
+ );
4937
+ }
4938
+ function campaignReviewDataHasUnsupportedSignalClaim(data) {
4939
+ const copyText = campaignReviewDataCopyParts(data).join(" ").toLowerCase();
4940
+ const claimsSignal = /\b(hiring|funding|raised|launched|expanding|appointed|announced|recent news|new role|job posting)\b/.test(copyText);
4941
+ return claimsSignal && !campaignReviewDataHasSignalBasis(data);
4942
+ }
4943
+ function campaignReviewDataHasApprovalLock(data) {
4944
+ const metadata = asRecord(data.fit_metadata ?? data.metadata);
4945
+ const approval = String(data.approval_status ?? data.delivery_approval_status ?? metadata.approval_status ?? "").toLowerCase();
4946
+ const exportStatus = String(data.export_status ?? data.exportStatus ?? "").toLowerCase();
4947
+ return data.export_ready === false || data.exportReady === false || data.delivery_approved === false || data.deliveryApproved === false || ["pending", "pending_approval", "blocked", "approval_required", "held"].includes(approval) || ["pending", "blocked", "approval_required", "held"].includes(exportStatus);
4948
+ }
4949
+ function campaignReviewDataHasHoldoutAttribution(data) {
4950
+ const fitMetadata = asRecord(data.fit_metadata ?? data.fitMetadata);
4951
+ const metadata = asRecord(data.metadata);
4952
+ const holdout = asRecord(
4953
+ fitMetadata.learning_holdout ?? fitMetadata.learningHoldout ?? metadata.learning_holdout ?? metadata.learningHoldout ?? data.learning_holdout ?? data.learningHoldout
4954
+ );
4955
+ const experimentKey = stringValue(holdout.experiment_key ?? holdout.experimentKey ?? data.experiment_key ?? data.experimentKey);
4956
+ const cohort = stringValue(holdout.cohort_role ?? holdout.cohortRole ?? data.cohort_role ?? data.learning_cohort ?? data.learningCohort);
4957
+ const brainApplied = holdout.brain_applied ?? holdout.brainApplied ?? data.brain_applied ?? data.signaliz_brain_applied;
4958
+ return Boolean(experimentKey && cohort && brainApplied !== void 0);
4959
+ }
4960
+ function campaignReviewStatusHasFeedbackReadiness(status) {
4961
+ const diagnostics = asRecord(status.diagnostics);
4962
+ const feedbackReadiness = asRecord(diagnostics.feedback_readiness ?? diagnostics.feedbackReadiness);
4963
+ const feedbackSubstrate = asRecord(diagnostics.feedback_substrate ?? diagnostics.feedbackSubstrate);
4964
+ const evidenceCounts = asRecord(diagnostics.evidence_counts ?? diagnostics.evidenceCounts);
4965
+ return feedbackReadiness.ready === true || feedbackReadiness.webhook_configured === true || feedbackSubstrate.stage === "ready" || Number(evidenceCounts.feedback_events ?? 0) > 0;
4966
+ }
4967
+ function campaignReviewMoatState(status, holdoutRows) {
4968
+ const diagnostics = asRecord(status.diagnostics);
4969
+ const evidenceCounts = asRecord(diagnostics.evidence_counts ?? diagnostics.evidenceCounts);
4970
+ const embeddedHoldout = asRecord(diagnostics.embedded_holdout ?? diagnostics.embeddedHoldout);
4971
+ const holdoutLift = asRecord(diagnostics.holdout_lift ?? diagnostics.holdoutLift ?? diagnostics.holdout_measurement ?? diagnostics.holdoutMeasurement);
4972
+ const controlSample = Math.max(
4973
+ Number(evidenceCounts.holdout_control_sample_size ?? 0),
4974
+ Number(evidenceCounts.embedded_holdout_control_sample ?? 0),
4975
+ Number(embeddedHoldout.control_sample_size ?? 0)
4976
+ );
4977
+ const learnedSample = Math.max(
4978
+ Number(evidenceCounts.holdout_learned_sample_size ?? 0),
4979
+ Number(evidenceCounts.embedded_holdout_learned_sample ?? 0),
4980
+ Number(embeddedHoldout.learned_sample_size ?? 0)
4981
+ );
4982
+ const feedbackEvents = Math.max(
4983
+ Number(evidenceCounts.feedback_events ?? 0),
4984
+ Number(evidenceCounts.campaign_build_feedback_events ?? 0),
4985
+ Number(evidenceCounts.delivery_holdout_sent_outcomes ?? 0)
4986
+ );
4987
+ const proved = diagnostics.closed_loop_proof_ready === true || holdoutLift.honest_measurement === true && holdoutLift.positive_lift === true;
4988
+ if (proved) {
4989
+ return {
4990
+ state: "proved",
4991
+ detail: "Real attributed feedback shows positive lift for explicit control and learned cohorts."
4992
+ };
4993
+ }
4994
+ if (controlSample > 0 && learnedSample > 0 && feedbackEvents > 0) {
4995
+ return {
4996
+ state: "moved_needs_lift_volume",
4997
+ detail: `Control and learned cohorts have attributed feedback (${controlSample} control, ${learnedSample} learned, ${feedbackEvents} feedback event(s)); keep collecting lift volume before claiming proof.`
4998
+ };
4999
+ }
5000
+ return {
5001
+ state: "scaffolding_ready",
5002
+ detail: holdoutRows > 0 ? `${holdoutRows} sampled row(s) carry holdout attribution, but real control/learned feedback lift is not proved yet.` : "Holdout and feedback scaffolding may be ready, but no real control/learned feedback lift is proved yet."
5003
+ };
5004
+ }
4426
5005
  function createCampaignBuilderReadinessLane(route, plan) {
4427
5006
  const builtIn = campaignBuilderBuiltInForRoute(route);
4428
5007
  const customRoute = route.provider !== "signaliz" && route.provider !== "csv" && builtIn === void 0;
@@ -7638,6 +8217,16 @@ var GtmKernel = class {
7638
8217
  min_privacy_k: input.minPrivacyK
7639
8218
  });
7640
8219
  }
8220
+ /** Queue a dry-run-first bridge from imported historical feedback to attribution-only Campaign Build surrogates. */
8221
+ async runHistoricalCampaignBuildBridge(input = {}) {
8222
+ return this.callMcp("gtm_historical_campaign_build_bridge_run", {
8223
+ dry_run: input.dryRun,
8224
+ write_approved: input.writeApproved,
8225
+ max_events: input.maxEvents,
8226
+ max_campaigns: input.maxCampaigns,
8227
+ min_feedback_events: input.minFeedbackEvents
8228
+ });
8229
+ }
7641
8230
  /** Prepare a provider-agnostic feedback webhook URL for Smartlead, HeyReach, Airbyte, or custom sender events. */
7642
8231
  async prepareFeedbackWebhook(input) {
7643
8232
  return this.callMcp("gtm_feedback_webhook_prepare", {