@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.
@@ -868,7 +868,11 @@ var Campaigns = class {
868
868
  maxSupportedTargetCount: data.max_supported_target_count,
869
869
  brainContext: data.brain_context ?? {},
870
870
  learningHoldout: data.learning_holdout ?? {},
871
- providerRoute: mapProviderRoute(data)
871
+ providerRoute: mapProviderRoute(data),
872
+ effectiveSourceRouting: recordOrNull(data.effective_source_routing ?? data.source_routing),
873
+ sourcePlan: recordOrNull(data.source_plan ?? data.plan?.source_plan),
874
+ sourceRoutes: recordArray(data.source_routes ?? data.source_plan?.routes ?? data.plan?.source_plan?.routes),
875
+ sourceShards: recordArray(data.source_shards ?? data.source_plan?.shards ?? data.plan?.source_plan?.shards)
872
876
  };
873
877
  }
874
878
  async getCampaignBuildStatus(campaignBuildId) {
@@ -1060,6 +1064,9 @@ function recordOrNull(value) {
1060
1064
  function stringArray(value) {
1061
1065
  return Array.isArray(value) ? value.map((item) => String(item || "").trim()).filter(Boolean) : [];
1062
1066
  }
1067
+ function recordArray(value) {
1068
+ return Array.isArray(value) ? value.map(recordOrNull).filter((item) => Boolean(item)) : [];
1069
+ }
1063
1070
  function diagnosticMessages(value) {
1064
1071
  if (!Array.isArray(value)) return [];
1065
1072
  return value.map((item) => {
@@ -1221,7 +1228,8 @@ function buildArgs(request) {
1221
1228
  suppress_prior_campaign_ids: request.cacheReusePolicy.suppressPriorCampaignIds,
1222
1229
  suppress_prior_list_ids: request.cacheReusePolicy.suppressPriorListIds,
1223
1230
  uniqueness: request.cacheReusePolicy.uniqueness,
1224
- require_usage_metadata: request.cacheReusePolicy.requireUsageMetadata
1231
+ require_usage_metadata: request.cacheReusePolicy.requireUsageMetadata,
1232
+ verified_cache_ttl_days: request.cacheReusePolicy.verifiedCacheTtlDays
1225
1233
  };
1226
1234
  }
1227
1235
  if (request.brainPreflight) args.brain_preflight = request.brainPreflight;
@@ -1904,6 +1912,7 @@ var CampaignBuilderAgent = class {
1904
1912
  },
1905
1913
  brain_defaults: plan.buildRequest.brainDefaults,
1906
1914
  brain_preflight: plan.buildRequest.brainPreflight,
1915
+ learning_holdout: plan.buildRequest.learningHoldout,
1907
1916
  campaign_strategy_context: plan.buildRequest.campaignStrategyContext,
1908
1917
  delivery_risk_preflight: plan.buildRequest.deliveryRisk,
1909
1918
  delivery_risk: plan.buildRequest.deliveryRisk
@@ -1917,7 +1926,8 @@ var CampaignBuilderAgent = class {
1917
1926
  estimated_credits: plan.estimatedCredits
1918
1927
  },
1919
1928
  brain_delivery_risk_preflight: plan.buildRequest.deliveryRisk,
1920
- delivery_risk: plan.buildRequest.deliveryRisk
1929
+ delivery_risk: plan.buildRequest.deliveryRisk,
1930
+ learning_holdout: plan.buildRequest.learningHoldout
1921
1931
  },
1922
1932
  approval_required: plan.approvals.some((approval) => approval.blocking),
1923
1933
  actor_type: "agent",
@@ -3316,6 +3326,21 @@ function routesFromCustomerTools(tools) {
3316
3326
  }))
3317
3327
  );
3318
3328
  }
3329
+ function normalizeCampaignBuilderLearningHoldout(input, gtmCampaignId, campaignName) {
3330
+ const source = asRecord(input);
3331
+ if (source.enabled === false) return { enabled: false };
3332
+ const controlPercentage = numberOrUndefined2(source.control_percentage ?? source.controlPercentage);
3333
+ const minControlSize = numberOrUndefined2(source.min_control_size ?? source.minControlSize);
3334
+ const experimentKey = stringValue(source.experiment_key ?? source.experimentKey) ?? (gtmCampaignId ? `gtm_campaign:${gtmCampaignId}:learning_holdout` : `campaign_builder_agent:${hashString(campaignName)}:learning_holdout`);
3335
+ return {
3336
+ enabled: true,
3337
+ controlPercentage: controlPercentage ?? 0.2,
3338
+ minControlSize: minControlSize ?? 50,
3339
+ experimentKey,
3340
+ sourceCampaignId: stringValue(source.source_campaign_id ?? source.sourceCampaignId) ?? gtmCampaignId,
3341
+ primaryMetric: stringValue(source.primary_metric ?? source.primaryMetric) ?? "positive_reply_rate"
3342
+ };
3343
+ }
3319
3344
  function createBuildRequest(request, defaults, targetCount, campaignName, scopeBuildArgs, routes = [], memory, scopeBrainPreflight) {
3320
3345
  const buildRequest = {
3321
3346
  name: campaignName,
@@ -3348,6 +3373,11 @@ function createBuildRequest(request, defaults, targetCount, campaignName, scopeB
3348
3373
  };
3349
3374
  const scoped = asRecord(scopeBuildArgs);
3350
3375
  buildRequest.gtmCampaignId = request.gtmCampaignId ?? stringValue(scoped.gtm_campaign_id ?? scoped.gtmCampaignId);
3376
+ buildRequest.learningHoldout = normalizeCampaignBuilderLearningHoldout(
3377
+ request.learningHoldout ?? scoped.learning_holdout ?? scoped.learningHoldout,
3378
+ buildRequest.gtmCampaignId,
3379
+ campaignName
3380
+ );
3351
3381
  if (typeof scoped.name === "string") buildRequest.name = scoped.name;
3352
3382
  if (typeof scoped.prompt === "string") buildRequest.prompt = scoped.prompt;
3353
3383
  if (typeof scoped.target_count === "number") buildRequest.targetCount = scoped.target_count;
@@ -3422,6 +3452,7 @@ function createBrainPreflight(request, buildRequest, routes, memory) {
3422
3452
  target_icp: targetIcp,
3423
3453
  provider_chain: providerChain,
3424
3454
  lead_source: providerChain[0] ?? "signaliz",
3455
+ learning_holdout: buildRequest.learningHoldout,
3425
3456
  tool_sequence: [
3426
3457
  { tool: "gtm_brain_learning_cycle_plan", purpose: "Plan the Brain learning loop for this campaign.", arguments: learningArgs },
3427
3458
  { tool: "gtm_brain_seed_defaults", purpose: "Seed campaign defaults from workspace memory and safe aggregate patterns.", arguments: defaultsArgs },
@@ -4089,6 +4120,16 @@ function buildCampaignArgs(request) {
4089
4120
  if (request.dedupKeys) args.dedup_keys = request.dedupKeys;
4090
4121
  if (request.brainPreflight) args.brain_preflight = request.brainPreflight;
4091
4122
  if (request.brainDefaults) args.brain_defaults = request.brainDefaults;
4123
+ if (request.learningHoldout) {
4124
+ args.learning_holdout = compact({
4125
+ enabled: request.learningHoldout.enabled,
4126
+ control_percentage: request.learningHoldout.controlPercentage,
4127
+ min_control_size: request.learningHoldout.minControlSize,
4128
+ experiment_key: request.learningHoldout.experimentKey,
4129
+ source_campaign_id: request.learningHoldout.sourceCampaignId,
4130
+ primary_metric: request.learningHoldout.primaryMetric
4131
+ });
4132
+ }
4092
4133
  if (request.campaignStrategyContext) args.campaign_strategy_context = request.campaignStrategyContext;
4093
4134
  if (request.deliveryRisk) args.delivery_risk = request.deliveryRisk;
4094
4135
  if (request.icp) {
@@ -4185,6 +4226,7 @@ function buildExecutionPrepareArgs(request, dryRun, confirmSpend) {
4185
4226
  delivery_risk: buildArgs2.delivery_risk,
4186
4227
  dedup_keys: buildArgs2.dedup_keys,
4187
4228
  policy: buildArgs2.policy,
4229
+ learning_holdout: buildArgs2.learning_holdout,
4188
4230
  signals: buildArgs2.signals,
4189
4231
  qualification: buildArgs2.qualification,
4190
4232
  copy: buildArgs2.copy,
@@ -4219,10 +4261,145 @@ function mapBuildResult(data, dryRun) {
4219
4261
  targetLimitReason: data.target_limit_reason,
4220
4262
  maxSupportedTargetCount: data.max_supported_target_count,
4221
4263
  brainContext: data.brain_context ?? {},
4264
+ learningHoldout: data.learning_holdout ?? {},
4222
4265
  providerRoute: mapProviderRoute2(data)
4223
4266
  };
4224
4267
  }
4268
+ var CAMPAIGN_BUILDER_TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "canceled", "cancelled"]);
4269
+ var CAMPAIGN_BUILDER_BLOCKED_TERMINAL_STATUSES = /* @__PURE__ */ new Set(["failed", "canceled", "cancelled"]);
4270
+ function mapAgentCampaignPhaseHealth(value) {
4271
+ if (!Array.isArray(value)) return [];
4272
+ return value.map((phase) => {
4273
+ const record = asRecord(phase);
4274
+ return {
4275
+ phase: stringValue(record.phase) ?? "unknown",
4276
+ status: stringValue(record.status) ?? "unknown",
4277
+ recordsProcessed: numberOrNull2(record.records_processed ?? record.recordsProcessed),
4278
+ recordsSucceeded: numberOrNull2(record.records_succeeded ?? record.recordsSucceeded),
4279
+ recordsFailed: numberOrNull2(record.records_failed ?? record.recordsFailed),
4280
+ heartbeatAgeSeconds: numberOrNull2(record.heartbeat_age_seconds ?? record.heartbeatAgeSeconds ?? record.phase_age_seconds)
4281
+ };
4282
+ });
4283
+ }
4284
+ function deriveCampaignBuildTerminalState(input) {
4285
+ const status = input.status.toLowerCase();
4286
+ const heartbeatAgeSeconds = input.phaseAgeSeconds;
4287
+ const stale = input.staleRunningPhase === true;
4288
+ const safeWatchAction = input.triggerRunId ? `signaliz ops run-status ${input.triggerRunId} --watch` : "signaliz campaign-agent status <campaign_build_id>";
4289
+ if (CAMPAIGN_BUILDER_BLOCKED_TERMINAL_STATUSES.has(status)) {
4290
+ return {
4291
+ kind: "blocked",
4292
+ label: "Blocked terminal state",
4293
+ reviewable: true,
4294
+ terminal: true,
4295
+ stale: false,
4296
+ phase: input.phase,
4297
+ phaseStatus: input.phaseStatus,
4298
+ heartbeatAgeSeconds,
4299
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4300
+ terminalReason: input.terminalReason,
4301
+ staleReason: null,
4302
+ safeNextAction: input.nextAction || "Review errors and rerun only after the blocker is resolved."
4303
+ };
4304
+ }
4305
+ if (status === "pending_approval") {
4306
+ return {
4307
+ kind: "pending_approval",
4308
+ label: "Pending delivery approval",
4309
+ reviewable: true,
4310
+ terminal: false,
4311
+ stale: false,
4312
+ phase: input.phase,
4313
+ phaseStatus: input.phaseStatus,
4314
+ heartbeatAgeSeconds,
4315
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4316
+ terminalReason: input.terminalReason,
4317
+ staleReason: null,
4318
+ safeNextAction: input.approvalAction || input.nextAction || "Review rows and approve delivery only after the approval packet passes."
4319
+ };
4320
+ }
4321
+ if (CAMPAIGN_BUILDER_TERMINAL_STATUSES.has(status)) {
4322
+ return {
4323
+ kind: "terminal",
4324
+ label: "Terminal and reviewable",
4325
+ reviewable: true,
4326
+ terminal: true,
4327
+ stale: false,
4328
+ phase: input.phase,
4329
+ phaseStatus: input.phaseStatus,
4330
+ heartbeatAgeSeconds,
4331
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4332
+ terminalReason: input.terminalReason,
4333
+ staleReason: null,
4334
+ safeNextAction: input.nextAction || "Review rows, artifacts, scorecard, and approval locks before delivery."
4335
+ };
4336
+ }
4337
+ if (status === "running" || status === "queued" || status === "processing") {
4338
+ if (stale) {
4339
+ return {
4340
+ kind: "running_stale",
4341
+ label: "Running but stale",
4342
+ reviewable: false,
4343
+ terminal: false,
4344
+ stale: true,
4345
+ phase: input.phase,
4346
+ phaseStatus: input.phaseStatus,
4347
+ heartbeatAgeSeconds,
4348
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4349
+ terminalReason: null,
4350
+ staleReason: input.staleReason || "The active phase heartbeat is stale.",
4351
+ safeNextAction: input.nextAction || safeWatchAction
4352
+ };
4353
+ }
4354
+ return {
4355
+ kind: "running_healthy",
4356
+ label: "Running and healthy",
4357
+ reviewable: false,
4358
+ terminal: false,
4359
+ stale: false,
4360
+ phase: input.phase,
4361
+ phaseStatus: input.phaseStatus,
4362
+ heartbeatAgeSeconds,
4363
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4364
+ terminalReason: null,
4365
+ staleReason: null,
4366
+ safeNextAction: input.nextAction || safeWatchAction
4367
+ };
4368
+ }
4369
+ return {
4370
+ kind: "unknown",
4371
+ label: "Unknown build state",
4372
+ reviewable: false,
4373
+ terminal: false,
4374
+ stale,
4375
+ phase: input.phase,
4376
+ phaseStatus: input.phaseStatus,
4377
+ heartbeatAgeSeconds,
4378
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4379
+ terminalReason: input.terminalReason,
4380
+ staleReason: input.staleReason,
4381
+ safeNextAction: input.nextAction || "Read campaign build status again before making a launch or delivery decision."
4382
+ };
4383
+ }
4225
4384
  function mapAgentCampaignBuildStatus(data) {
4385
+ const customerRowCounts = mapAgentCustomerRowCounts(data.customer_row_counts);
4386
+ const phaseHealth = mapAgentCampaignPhaseHealth(data.phases);
4387
+ const terminalReason = stringValue(data.terminal_reason) ?? customerRowCounts?.terminalReason ?? null;
4388
+ const staleReason = stringValue(data.stale_reason) ?? stringValue(asRecord(data.diagnostics).stale_reason) ?? null;
4389
+ const nextPollAfterSeconds = numberOrNull2(data.next_poll_after_seconds ?? data.nextPollAfterSeconds);
4390
+ const terminalState = deriveCampaignBuildTerminalState({
4391
+ status: stringValue(data.status) ?? "unknown",
4392
+ phase: stringValue(data.current_phase) ?? null,
4393
+ phaseStatus: stringValue(data.current_phase_status) ?? null,
4394
+ staleRunningPhase: data.stale_running_phase === true,
4395
+ phaseAgeSeconds: numberOrNull2(data.phase_age_seconds),
4396
+ nextPollAfterSeconds,
4397
+ terminalReason,
4398
+ staleReason,
4399
+ nextAction: formatAgentNextAction(data.next_action),
4400
+ approvalAction: formatAgentNextAction(data.approval_action),
4401
+ triggerRunId: stringValue(data.trigger_run_id) ?? null
4402
+ });
4226
4403
  return {
4227
4404
  campaignBuildId: stringValue(data.campaign_build_id) ?? "",
4228
4405
  campaignId: stringValue(data.campaign_id) ?? null,
@@ -4240,12 +4417,18 @@ function mapAgentCampaignBuildStatus(data) {
4240
4417
  errors: diagnosticMessages2(data.errors),
4241
4418
  artifactCount: numberOrUndefined2(data.artifact_count) ?? 0,
4242
4419
  artifactDownloads: Array.isArray(data.artifact_downloads) ? data.artifact_downloads.map(mapAgentCampaignArtifactDownload) : [],
4243
- customerRowCounts: mapAgentCustomerRowCounts(data.customer_row_counts),
4420
+ customerRowCounts,
4244
4421
  phaseAttemptTotals: nullableRecord(data.phase_attempt_totals) ?? void 0,
4245
4422
  providerRoute: mapProviderRoute2(data),
4246
4423
  triggerRunId: stringValue(data.trigger_run_id) ?? null,
4247
4424
  staleRunningPhase: data.stale_running_phase === true,
4248
4425
  phaseAgeSeconds: numberOrNull2(data.phase_age_seconds),
4426
+ nextPollAfterSeconds,
4427
+ terminalReason,
4428
+ staleReason,
4429
+ safeNextAction: terminalState.safeNextAction,
4430
+ terminalState,
4431
+ phaseHealth,
4249
4432
  diagnostics: nullableRecord(data.diagnostics) ?? {},
4250
4433
  nextAction: formatAgentNextAction(data.next_action),
4251
4434
  approvalAction: formatAgentNextAction(data.approval_action),
@@ -4333,67 +4516,285 @@ function mapAgentCampaignArtifact(data) {
4333
4516
  createdAt: stringValue(data.created_at) ?? ""
4334
4517
  };
4335
4518
  }
4519
+ var NORTH_STAR_BANNED_COPY_PHRASES = [
4520
+ "i have been following",
4521
+ "i saw that",
4522
+ "would you be opposed",
4523
+ "unlock new opportunities",
4524
+ "significant success",
4525
+ "we understand",
4526
+ "hope this finds you well"
4527
+ ];
4528
+ function campaignReviewGate(id, label, status, detail) {
4529
+ return {
4530
+ id,
4531
+ label,
4532
+ status,
4533
+ passed: status !== "blocked",
4534
+ detail
4535
+ };
4536
+ }
4537
+ function northStarMetric(id, label, status, detail, value, target) {
4538
+ return {
4539
+ id,
4540
+ label,
4541
+ status,
4542
+ detail,
4543
+ ...value !== void 0 ? { value } : {},
4544
+ ...target !== void 0 ? { target } : {}
4545
+ };
4546
+ }
4547
+ function createCampaignBuilderNorthStarScorecard(status, rows, artifacts, terminalState) {
4548
+ if (!terminalState.reviewable) {
4549
+ const pendingMetric = northStarMetric(
4550
+ "terminal_state",
4551
+ "Terminal-state contract",
4552
+ terminalState.kind === "running_stale" ? "blocked" : "pending",
4553
+ 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.`,
4554
+ terminalState.kind,
4555
+ "terminal or pending_approval"
4556
+ );
4557
+ return {
4558
+ version: "campaign-builder-north-star.v1",
4559
+ status: pendingMetric.status,
4560
+ score: null,
4561
+ moatState: "scaffolding_ready",
4562
+ metrics: [pendingMetric],
4563
+ blockers: pendingMetric.status === "blocked" ? [pendingMetric.detail] : [],
4564
+ warnings: pendingMetric.status === "pending" ? [pendingMetric.detail] : [],
4565
+ nextAction: terminalState.safeNextAction
4566
+ };
4567
+ }
4568
+ const sampledRows = rows.rows.length;
4569
+ const rowData = rows.rows.map((row) => asRecord(row.data));
4570
+ const requestedTarget = status.customerRowCounts?.requestedTarget ?? status.recordsTotal ?? rows.count;
4571
+ const finalRows = status.customerRowCounts?.deliveredRows ?? status.customerRowCounts?.qaReadyRows ?? status.customerRowCounts?.copiedRows ?? status.customerRowCounts?.acceptedRows ?? rows.count;
4572
+ const rowsWithEmail = rows.rows.filter(campaignReviewRowHasEmail).length;
4573
+ const verifiedEmailRows = rowData.filter(campaignReviewDataHasVerifiedEmail).length;
4574
+ const uniqueEmails = new Set(rowData.map(campaignReviewDataEmail).filter(Boolean)).size;
4575
+ const uniqueDomains = new Set(rowData.map(campaignReviewDataDomain).filter(Boolean)).size;
4576
+ const copyReadyRows = rows.rows.filter(campaignReviewRowHasCopy).length;
4577
+ const copyQaIssuesByRow = rowData.map(campaignReviewDataCopyQaIssues).filter((issues) => issues.length > 0);
4578
+ const copyQaIssueCounts = countCampaignReviewIssues(copyQaIssuesByRow);
4579
+ const signalBackedRows = rowData.filter(campaignReviewDataHasSignalBasis).length;
4580
+ const noSupportedSignalRows = rowData.filter(campaignReviewDataHasNoSupportedSignal).length;
4581
+ const unsupportedSignalClaims = rowData.filter(campaignReviewDataHasUnsupportedSignalClaim).length;
4582
+ const approvalLockedRows = rowData.filter(campaignReviewDataHasApprovalLock).length;
4583
+ const holdoutRows = rowData.filter(campaignReviewDataHasHoldoutAttribution).length;
4584
+ const artifactCount = artifacts.length || status.artifactCount || 0;
4585
+ const deliveredRows = status.customerRowCounts?.deliveredRows ?? 0;
4586
+ const feedbackReady = campaignReviewStatusHasFeedbackReadiness(status);
4587
+ const feedbackRequiredForApproval = terminalState.kind === "pending_approval";
4588
+ const moat = campaignReviewMoatState(status, holdoutRows);
4589
+ const metrics = [
4590
+ northStarMetric(
4591
+ "terminal_state",
4592
+ "Terminal-state contract",
4593
+ terminalState.kind === "blocked" ? "blocked" : "pass",
4594
+ terminalState.label,
4595
+ terminalState.kind,
4596
+ "terminal or pending_approval"
4597
+ ),
4598
+ northStarMetric(
4599
+ "target_fill",
4600
+ "Target fill",
4601
+ requestedTarget && finalRows < requestedTarget ? "review" : "pass",
4602
+ requestedTarget ? `${finalRows || 0}/${requestedTarget} final or reviewable row(s) are accounted for.` : "No requested target was reported; using sampled rows for review.",
4603
+ finalRows || sampledRows,
4604
+ requestedTarget || sampledRows
4605
+ ),
4606
+ northStarMetric(
4607
+ "unique_emails",
4608
+ "Unique emails",
4609
+ sampledRows === 0 ? "blocked" : uniqueEmails === rowsWithEmail ? "pass" : "blocked",
4610
+ sampledRows === 0 ? "No sampled rows were available for email uniqueness review." : `${uniqueEmails}/${rowsWithEmail} sampled email(s) are unique.`,
4611
+ uniqueEmails,
4612
+ rowsWithEmail
4613
+ ),
4614
+ northStarMetric(
4615
+ "unique_domains",
4616
+ "Unique domains",
4617
+ sampledRows === 0 ? "blocked" : uniqueDomains === sampledRows ? "pass" : "review",
4618
+ sampledRows === 0 ? "No sampled rows were available for domain uniqueness review." : `${uniqueDomains}/${sampledRows} sampled company domain(s) are unique.`,
4619
+ uniqueDomains,
4620
+ sampledRows
4621
+ ),
4622
+ northStarMetric(
4623
+ "verified_email_rows",
4624
+ "Verified email rows",
4625
+ sampledRows === 0 ? "blocked" : verifiedEmailRows === sampledRows ? "pass" : rowsWithEmail === sampledRows ? "review" : "blocked",
4626
+ sampledRows === 0 ? "No sampled rows were available for email verification review." : `${verifiedEmailRows}/${sampledRows} sampled row(s) carry verified-email evidence.`,
4627
+ verifiedEmailRows,
4628
+ sampledRows
4629
+ ),
4630
+ northStarMetric(
4631
+ "copy_completeness",
4632
+ "Copy completeness",
4633
+ sampledRows === 0 ? "blocked" : copyReadyRows === sampledRows ? "pass" : "blocked",
4634
+ sampledRows === 0 ? "No sampled rows were available for copy review." : `${copyReadyRows}/${sampledRows} sampled row(s) include generated copy.`,
4635
+ copyReadyRows,
4636
+ sampledRows
4637
+ ),
4638
+ northStarMetric(
4639
+ "copy_quality",
4640
+ "Copy QA",
4641
+ copyQaIssuesByRow.length === 0 ? "pass" : "blocked",
4642
+ copyQaIssuesByRow.length === 0 ? "No sampled copy QA blockers were detected." : `${copyQaIssuesByRow.length}/${sampledRows} sampled row(s) have copy QA issue(s): ${formatCampaignReviewIssueCounts(copyQaIssueCounts)}.`,
4643
+ copyQaIssuesByRow.length,
4644
+ 0
4645
+ ),
4646
+ northStarMetric(
4647
+ "signal_grounding",
4648
+ "Signal grounding",
4649
+ unsupportedSignalClaims > 0 ? "blocked" : signalBackedRows > 0 ? "pass" : "review",
4650
+ 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.",
4651
+ signalBackedRows,
4652
+ sampledRows
4653
+ ),
4654
+ northStarMetric(
4655
+ "approval_locks",
4656
+ "Approval locks",
4657
+ approvalLockedRows > 0 || deliveredRows === 0 ? "pass" : "review",
4658
+ 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.",
4659
+ approvalLockedRows,
4660
+ sampledRows
4661
+ ),
4662
+ northStarMetric(
4663
+ "export_send_safety",
4664
+ "Export/send safety",
4665
+ deliveredRows > 0 && status.status !== "completed" ? "blocked" : "pass",
4666
+ deliveredRows > 0 ? `${deliveredRows} row(s) are delivered; status is ${status.status}.` : "No delivered rows are reported before approval.",
4667
+ deliveredRows,
4668
+ 0
4669
+ ),
4670
+ northStarMetric(
4671
+ "holdout_attribution",
4672
+ "Holdout attribution",
4673
+ holdoutRows > 0 ? "pass" : "review",
4674
+ holdoutRows > 0 ? `${holdoutRows}/${sampledRows} sampled row(s) preserve holdout attribution.` : "No holdout attribution was visible in the sampled rows.",
4675
+ holdoutRows,
4676
+ sampledRows
4677
+ ),
4678
+ northStarMetric(
4679
+ "feedback_readiness",
4680
+ "Feedback readiness",
4681
+ feedbackReady ? "pass" : feedbackRequiredForApproval ? "blocked" : "review",
4682
+ 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.",
4683
+ feedbackReady,
4684
+ true
4685
+ ),
4686
+ northStarMetric(
4687
+ "causal_moat",
4688
+ "Causal moat",
4689
+ moat.state === "proved" ? "pass" : "review",
4690
+ moat.detail,
4691
+ moat.state,
4692
+ "proved"
4693
+ ),
4694
+ northStarMetric(
4695
+ "artifact_readback",
4696
+ "Artifact readback",
4697
+ artifactCount > 0 ? "pass" : "review",
4698
+ artifactCount > 0 ? `${artifactCount} artifact(s) are available for review.` : "No review artifact is available yet.",
4699
+ artifactCount,
4700
+ 1
4701
+ )
4702
+ ];
4703
+ const gradedMetrics = metrics.filter((metric) => metric.status !== "pending");
4704
+ const score = gradedMetrics.length === 0 ? null : Math.round(gradedMetrics.filter((metric) => metric.status === "pass").length / gradedMetrics.length * 100);
4705
+ const blockers = metrics.filter((metric) => metric.status === "blocked").map((metric) => metric.detail);
4706
+ const warnings = metrics.filter((metric) => metric.status === "review").map((metric) => metric.detail);
4707
+ const scorecardStatus = blockers.length > 0 ? "blocked" : warnings.length > 0 ? "review" : "pass";
4708
+ return {
4709
+ version: "campaign-builder-north-star.v1",
4710
+ status: scorecardStatus,
4711
+ score,
4712
+ moatState: moat.state,
4713
+ metrics,
4714
+ blockers,
4715
+ warnings,
4716
+ nextAction: scorecardStatus === "blocked" ? "Fix blocked North Star gates before approval or delivery." : terminalState.safeNextAction
4717
+ };
4718
+ }
4336
4719
  function createCampaignBuilderBuildReview(status, rows, artifacts) {
4720
+ const terminalState = status.terminalState ?? deriveCampaignBuildTerminalState({
4721
+ status: status.status,
4722
+ phase: status.currentPhase,
4723
+ phaseStatus: status.currentPhaseStatus,
4724
+ staleRunningPhase: status.staleRunningPhase === true,
4725
+ phaseAgeSeconds: status.phaseAgeSeconds ?? null,
4726
+ nextPollAfterSeconds: status.nextPollAfterSeconds ?? null,
4727
+ terminalReason: status.terminalReason ?? status.customerRowCounts?.terminalReason ?? null,
4728
+ staleReason: status.staleReason ?? null,
4729
+ nextAction: status.nextAction,
4730
+ approvalAction: status.approvalAction,
4731
+ triggerRunId: status.triggerRunId ?? null
4732
+ });
4337
4733
  const sampledRows = rows.rows.length;
4338
4734
  const availableRows = rows.count || sampledRows;
4339
4735
  const qualifiedRows = rows.rows.filter((row) => row.qualified !== false).length;
4340
4736
  const rowsWithEmail = rows.rows.filter(campaignReviewRowHasEmail).length;
4341
4737
  const copyReadyRows = rows.rows.filter(campaignReviewRowHasCopy).length;
4342
4738
  const artifactCount = artifacts.length || status.artifactCount || 0;
4739
+ const northStarScorecard = createCampaignBuilderNorthStarScorecard(status, rows, artifacts, terminalState);
4343
4740
  const gates = [
4344
- {
4345
- id: "build_completed",
4346
- label: "Build completed",
4347
- passed: status.status === "completed",
4348
- detail: status.status === "completed" ? "The campaign build is completed." : `The campaign build is ${status.status}; review again after completion.`
4349
- },
4350
- {
4351
- id: "rows_available",
4352
- label: "Rows available",
4353
- passed: sampledRows > 0,
4354
- detail: sampledRows > 0 ? `${sampledRows} sampled row(s) are available for review.` : "No rows were returned for review."
4355
- },
4356
- {
4357
- id: "qualified_sample",
4358
- label: "Sample rows qualified",
4359
- passed: sampledRows > 0 && qualifiedRows === sampledRows,
4360
- detail: sampledRows > 0 ? `${qualifiedRows}/${sampledRows} sampled row(s) are marked qualified.` : "Qualification could not be checked without sampled rows."
4361
- },
4362
- {
4363
- id: "email_coverage",
4364
- label: "Email coverage",
4365
- passed: sampledRows > 0 && rowsWithEmail === sampledRows,
4366
- detail: sampledRows > 0 ? `${rowsWithEmail}/${sampledRows} sampled row(s) include an email.` : "Email coverage could not be checked without sampled rows."
4367
- },
4368
- {
4369
- id: "artifact_available",
4370
- label: "Artifact available",
4371
- passed: artifactCount > 0,
4372
- detail: artifactCount > 0 ? `${artifactCount} artifact(s) are available.` : "No delivery artifact is available yet."
4373
- },
4374
- {
4375
- id: "no_failed_rows",
4376
- label: "No failed rows",
4377
- passed: status.recordsFailed === 0,
4378
- detail: status.recordsFailed === 0 ? "The build reports zero failed rows." : `${status.recordsFailed} row(s) failed during the build.`
4379
- }
4741
+ campaignReviewGate(
4742
+ "build_reviewable",
4743
+ "Build reviewable",
4744
+ terminalState.kind === "running_healthy" ? "pending" : terminalState.kind === "running_stale" || terminalState.kind === "blocked" ? "blocked" : "pass",
4745
+ terminalState.label
4746
+ ),
4747
+ campaignReviewGate(
4748
+ "rows_available",
4749
+ "Rows available",
4750
+ sampledRows > 0 ? "pass" : terminalState.reviewable ? "blocked" : "pending",
4751
+ 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."
4752
+ ),
4753
+ campaignReviewGate(
4754
+ "qualified_sample",
4755
+ "Sample rows qualified",
4756
+ sampledRows === 0 ? terminalState.reviewable ? "blocked" : "pending" : qualifiedRows === sampledRows ? "pass" : "blocked",
4757
+ sampledRows > 0 ? `${qualifiedRows}/${sampledRows} sampled row(s) are marked qualified.` : "Qualification could not be checked without sampled rows."
4758
+ ),
4759
+ campaignReviewGate(
4760
+ "email_coverage",
4761
+ "Email coverage",
4762
+ sampledRows === 0 ? terminalState.reviewable ? "blocked" : "pending" : rowsWithEmail === sampledRows ? "pass" : "blocked",
4763
+ sampledRows > 0 ? `${rowsWithEmail}/${sampledRows} sampled row(s) include an email.` : "Email coverage could not be checked without sampled rows."
4764
+ ),
4765
+ campaignReviewGate(
4766
+ "artifact_available",
4767
+ "Artifact available",
4768
+ artifactCount > 0 ? "pass" : terminalState.reviewable ? "review" : "pending",
4769
+ artifactCount > 0 ? `${artifactCount} artifact(s) are available.` : "No delivery artifact is available yet."
4770
+ ),
4771
+ campaignReviewGate(
4772
+ "no_failed_rows",
4773
+ "No failed rows",
4774
+ status.recordsFailed === 0 ? "pass" : "blocked",
4775
+ status.recordsFailed === 0 ? "The build reports zero failed rows." : `${status.recordsFailed} row(s) failed during the build.`
4776
+ )
4380
4777
  ];
4381
- const blockedReasons = gates.filter((gate) => !gate.passed).map((gate) => gate.detail);
4778
+ const blockedReasons = gates.filter((gate) => gate.status === "blocked" || !gate.passed).map((gate) => gate.detail);
4382
4779
  const warnings = [
4383
4780
  rows.hasMore ? "This review is based on a row sample; page through remaining rows before final approval." : void 0,
4384
4781
  sampledRows > 0 && copyReadyRows < sampledRows ? `${copyReadyRows}/${sampledRows} sampled row(s) include generated copy.` : void 0,
4385
4782
  ...status.warnings
4386
4783
  ].filter((item) => Boolean(item));
4387
- const readyForDeliveryApproval = blockedReasons.length === 0;
4784
+ const readyForDeliveryApproval = terminalState.reviewable && blockedReasons.length === 0 && northStarScorecard.status !== "blocked";
4388
4785
  return {
4389
4786
  campaignBuildId: status.campaignBuildId || rows.campaignBuildId,
4390
4787
  status,
4391
4788
  rows,
4392
4789
  artifacts,
4393
4790
  gates,
4791
+ northStarScorecard,
4394
4792
  summary: {
4395
4793
  status: status.status,
4396
4794
  phase: status.currentPhase,
4795
+ terminalState: terminalState.kind,
4796
+ scorecardStatus: northStarScorecard.status,
4797
+ scorecardScore: northStarScorecard.score,
4397
4798
  sampledRows,
4398
4799
  availableRows,
4399
4800
  qualifiedRows,
@@ -4447,6 +4848,184 @@ function campaignReviewRowHasCopy(row) {
4447
4848
  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)
4448
4849
  );
4449
4850
  }
4851
+ function campaignReviewDataEmail(data) {
4852
+ return stringValue(data.email) || stringValue(data.work_email) || stringValue(data.workEmail) || stringValue(asRecord(data.contact).email);
4853
+ }
4854
+ function campaignReviewDataDomain(data) {
4855
+ const domain = stringValue(data.company_domain) || stringValue(data.companyDomain) || stringValue(asRecord(data.company).domain);
4856
+ return domain?.toLowerCase();
4857
+ }
4858
+ function campaignReviewDataHasVerifiedEmail(data) {
4859
+ const status = String(data.email_status ?? data.emailStatus ?? asRecord(data.contact).email_status ?? "").toLowerCase();
4860
+ return data.email_verified === true || data.emailVerified === true || asRecord(data.contact).email_verified === true || ["valid", "validated", "verified"].includes(status);
4861
+ }
4862
+ function campaignReviewDataCopyParts(data) {
4863
+ const copy = asRecord(data.copy);
4864
+ const rawCopy = asRecord(data.raw_copy ?? data.rawCopy);
4865
+ return [
4866
+ stringValue(data.subject),
4867
+ stringValue(data.subject_line),
4868
+ stringValue(data.opener),
4869
+ stringValue(data.body),
4870
+ stringValue(data.cta),
4871
+ stringValue(copy.subject),
4872
+ stringValue(copy.opener),
4873
+ stringValue(copy.body),
4874
+ stringValue(copy.cta),
4875
+ stringValue(rawCopy.subject),
4876
+ stringValue(rawCopy.opener),
4877
+ stringValue(rawCopy.body),
4878
+ stringValue(rawCopy.cta)
4879
+ ].filter((part) => Boolean(part));
4880
+ }
4881
+ function campaignReviewDataHasBadGreeting(data) {
4882
+ return campaignReviewDataCopyParts(data).some(
4883
+ (part) => /\bhi\s*(?:,|\{\{|\[)|\bhello\s*(?:,|\{\{|\[)/i.test(part) || /\b(first_name|firstname|name)\b/i.test(part)
4884
+ );
4885
+ }
4886
+ function campaignReviewDataHasBannedPhrase(data) {
4887
+ const text = campaignReviewDataCopyParts(data).join(" ").toLowerCase();
4888
+ return NORTH_STAR_BANNED_COPY_PHRASES.some((phrase) => text.includes(phrase));
4889
+ }
4890
+ function campaignReviewDataSubject(data) {
4891
+ const copy = asRecord(data.copy);
4892
+ const rawCopy = asRecord(data.raw_copy ?? data.rawCopy);
4893
+ return stringValue(data.subject) || stringValue(data.subject_line) || stringValue(data.subjectLine) || stringValue(copy.subject) || stringValue(rawCopy.subject);
4894
+ }
4895
+ function campaignReviewDataCta(data) {
4896
+ const copy = asRecord(data.copy);
4897
+ const rawCopy = asRecord(data.raw_copy ?? data.rawCopy);
4898
+ return stringValue(data.cta) || stringValue(copy.cta) || stringValue(rawCopy.cta);
4899
+ }
4900
+ function countCampaignReviewIssues(issueRows) {
4901
+ return issueRows.flat().reduce((acc, issue) => {
4902
+ acc[issue] = (acc[issue] || 0) + 1;
4903
+ return acc;
4904
+ }, {});
4905
+ }
4906
+ function formatCampaignReviewIssueCounts(counts) {
4907
+ const entries = Object.entries(counts);
4908
+ return entries.length > 0 ? entries.map(([issue, count]) => `${issue}=${count}`).join(", ") : "none";
4909
+ }
4910
+ function campaignReviewDataCopyQaIssues(data) {
4911
+ const issues = /* @__PURE__ */ new Set();
4912
+ const copyParts = campaignReviewDataCopyParts(data);
4913
+ if (copyParts.length === 0) return [];
4914
+ if (campaignReviewDataHasBadGreeting(data)) issues.add("bad_greeting");
4915
+ if (campaignReviewDataHasBannedPhrase(data)) issues.add("banned_or_vague_phrase");
4916
+ if (campaignReviewDataHasUnsupportedSignalClaim(data)) issues.add("unsupported_signal_claim");
4917
+ if (campaignReviewDataPersonalizationBasis(data).length === 0) issues.add("missing_personalization_basis");
4918
+ if (!campaignReviewDataCta(data)) issues.add("missing_cta");
4919
+ const subject = campaignReviewDataSubject(data);
4920
+ if (subject && subject.length > 70) issues.add("overlong_subject");
4921
+ return [...issues];
4922
+ }
4923
+ function campaignReviewDataPersonalizationBasis(data) {
4924
+ const rawCopy = asRecord(data.raw_copy ?? data.rawCopy);
4925
+ const copy = asRecord(data.copy);
4926
+ const metadata = asRecord(data.metadata);
4927
+ const signalEvidence = asRecord(data.signal_evidence ?? metadata.signal_evidence ?? metadata.signalEvidence);
4928
+ return [
4929
+ ...arrayOfStrings(data.personalization_basis) ?? [],
4930
+ ...arrayOfStrings(data.personalizationBasis) ?? [],
4931
+ ...arrayOfStrings(copy.personalization_basis) ?? [],
4932
+ ...arrayOfStrings(rawCopy.personalization_basis) ?? [],
4933
+ ...arrayOfStrings(metadata.personalization_basis) ?? [],
4934
+ ...arrayOfStrings(metadata.personalizationBasis) ?? [],
4935
+ ...arrayOfStrings(signalEvidence.personalization_basis) ?? []
4936
+ ];
4937
+ }
4938
+ function campaignReviewDataHasNoSupportedSignal(data) {
4939
+ const metadata = asRecord(data.metadata);
4940
+ const copy = asRecord(data.copy);
4941
+ const state = [
4942
+ data.copy_signal_state,
4943
+ data.signal_basis,
4944
+ data.signal_type,
4945
+ metadata.copy_signal_state,
4946
+ metadata.signal_basis,
4947
+ metadata.signal_type,
4948
+ copy.copy_signal_state
4949
+ ].map((value) => stringValue(value)?.toLowerCase()).filter(Boolean);
4950
+ return data.no_supported_signal === true || metadata.no_supported_signal === true || state.includes("no_supported_signal");
4951
+ }
4952
+ function campaignReviewDataHasSignalBasis(data) {
4953
+ if (campaignReviewDataHasNoSupportedSignal(data)) return false;
4954
+ const metadata = asRecord(data.metadata);
4955
+ const signal = asRecord(data.signal);
4956
+ const signalEvidence = asRecord(data.signal_evidence ?? metadata.signal_evidence ?? metadata.signalEvidence);
4957
+ const basis = campaignReviewDataPersonalizationBasis(data);
4958
+ return Boolean(
4959
+ 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))
4960
+ );
4961
+ }
4962
+ function campaignReviewDataHasUnsupportedSignalClaim(data) {
4963
+ const copyText = campaignReviewDataCopyParts(data).join(" ").toLowerCase();
4964
+ const claimsSignal = /\b(hiring|funding|raised|launched|expanding|appointed|announced|recent news|new role|job posting)\b/.test(copyText);
4965
+ return claimsSignal && !campaignReviewDataHasSignalBasis(data);
4966
+ }
4967
+ function campaignReviewDataHasApprovalLock(data) {
4968
+ const metadata = asRecord(data.fit_metadata ?? data.metadata);
4969
+ const approval = String(data.approval_status ?? data.delivery_approval_status ?? metadata.approval_status ?? "").toLowerCase();
4970
+ const exportStatus = String(data.export_status ?? data.exportStatus ?? "").toLowerCase();
4971
+ 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);
4972
+ }
4973
+ function campaignReviewDataHasHoldoutAttribution(data) {
4974
+ const fitMetadata = asRecord(data.fit_metadata ?? data.fitMetadata);
4975
+ const metadata = asRecord(data.metadata);
4976
+ const holdout = asRecord(
4977
+ fitMetadata.learning_holdout ?? fitMetadata.learningHoldout ?? metadata.learning_holdout ?? metadata.learningHoldout ?? data.learning_holdout ?? data.learningHoldout
4978
+ );
4979
+ const experimentKey = stringValue(holdout.experiment_key ?? holdout.experimentKey ?? data.experiment_key ?? data.experimentKey);
4980
+ const cohort = stringValue(holdout.cohort_role ?? holdout.cohortRole ?? data.cohort_role ?? data.learning_cohort ?? data.learningCohort);
4981
+ const brainApplied = holdout.brain_applied ?? holdout.brainApplied ?? data.brain_applied ?? data.signaliz_brain_applied;
4982
+ return Boolean(experimentKey && cohort && brainApplied !== void 0);
4983
+ }
4984
+ function campaignReviewStatusHasFeedbackReadiness(status) {
4985
+ const diagnostics = asRecord(status.diagnostics);
4986
+ const feedbackReadiness = asRecord(diagnostics.feedback_readiness ?? diagnostics.feedbackReadiness);
4987
+ const feedbackSubstrate = asRecord(diagnostics.feedback_substrate ?? diagnostics.feedbackSubstrate);
4988
+ const evidenceCounts = asRecord(diagnostics.evidence_counts ?? diagnostics.evidenceCounts);
4989
+ return feedbackReadiness.ready === true || feedbackReadiness.webhook_configured === true || feedbackSubstrate.stage === "ready" || Number(evidenceCounts.feedback_events ?? 0) > 0;
4990
+ }
4991
+ function campaignReviewMoatState(status, holdoutRows) {
4992
+ const diagnostics = asRecord(status.diagnostics);
4993
+ const evidenceCounts = asRecord(diagnostics.evidence_counts ?? diagnostics.evidenceCounts);
4994
+ const embeddedHoldout = asRecord(diagnostics.embedded_holdout ?? diagnostics.embeddedHoldout);
4995
+ const holdoutLift = asRecord(diagnostics.holdout_lift ?? diagnostics.holdoutLift ?? diagnostics.holdout_measurement ?? diagnostics.holdoutMeasurement);
4996
+ const controlSample = Math.max(
4997
+ Number(evidenceCounts.holdout_control_sample_size ?? 0),
4998
+ Number(evidenceCounts.embedded_holdout_control_sample ?? 0),
4999
+ Number(embeddedHoldout.control_sample_size ?? 0)
5000
+ );
5001
+ const learnedSample = Math.max(
5002
+ Number(evidenceCounts.holdout_learned_sample_size ?? 0),
5003
+ Number(evidenceCounts.embedded_holdout_learned_sample ?? 0),
5004
+ Number(embeddedHoldout.learned_sample_size ?? 0)
5005
+ );
5006
+ const feedbackEvents = Math.max(
5007
+ Number(evidenceCounts.feedback_events ?? 0),
5008
+ Number(evidenceCounts.campaign_build_feedback_events ?? 0),
5009
+ Number(evidenceCounts.delivery_holdout_sent_outcomes ?? 0)
5010
+ );
5011
+ const proved = diagnostics.closed_loop_proof_ready === true || holdoutLift.honest_measurement === true && holdoutLift.positive_lift === true;
5012
+ if (proved) {
5013
+ return {
5014
+ state: "proved",
5015
+ detail: "Real attributed feedback shows positive lift for explicit control and learned cohorts."
5016
+ };
5017
+ }
5018
+ if (controlSample > 0 && learnedSample > 0 && feedbackEvents > 0) {
5019
+ return {
5020
+ state: "moved_needs_lift_volume",
5021
+ detail: `Control and learned cohorts have attributed feedback (${controlSample} control, ${learnedSample} learned, ${feedbackEvents} feedback event(s)); keep collecting lift volume before claiming proof.`
5022
+ };
5023
+ }
5024
+ return {
5025
+ state: "scaffolding_ready",
5026
+ 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."
5027
+ };
5028
+ }
4450
5029
  function createCampaignBuilderReadinessLane(route, plan) {
4451
5030
  const builtIn = campaignBuilderBuiltInForRoute(route);
4452
5031
  const customRoute = route.provider !== "signaliz" && route.provider !== "csv" && builtIn === void 0;
@@ -7662,6 +8241,16 @@ var GtmKernel = class {
7662
8241
  min_privacy_k: input.minPrivacyK
7663
8242
  });
7664
8243
  }
8244
+ /** Queue a dry-run-first bridge from imported historical feedback to attribution-only Campaign Build surrogates. */
8245
+ async runHistoricalCampaignBuildBridge(input = {}) {
8246
+ return this.callMcp("gtm_historical_campaign_build_bridge_run", {
8247
+ dry_run: input.dryRun,
8248
+ write_approved: input.writeApproved,
8249
+ max_events: input.maxEvents,
8250
+ max_campaigns: input.maxCampaigns,
8251
+ min_feedback_events: input.minFeedbackEvents
8252
+ });
8253
+ }
7665
8254
  /** Prepare a provider-agnostic feedback webhook URL for Smartlead, HeyReach, Airbyte, or custom sender events. */
7666
8255
  async prepareFeedbackWebhook(input) {
7667
8256
  return this.callMcp("gtm_feedback_webhook_prepare", {