@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.
package/dist/index.js CHANGED
@@ -890,7 +890,11 @@ var Campaigns = class {
890
890
  maxSupportedTargetCount: data.max_supported_target_count,
891
891
  brainContext: data.brain_context ?? {},
892
892
  learningHoldout: data.learning_holdout ?? {},
893
- providerRoute: mapProviderRoute(data)
893
+ providerRoute: mapProviderRoute(data),
894
+ effectiveSourceRouting: recordOrNull(data.effective_source_routing ?? data.source_routing),
895
+ sourcePlan: recordOrNull(data.source_plan ?? data.plan?.source_plan),
896
+ sourceRoutes: recordArray(data.source_routes ?? data.source_plan?.routes ?? data.plan?.source_plan?.routes),
897
+ sourceShards: recordArray(data.source_shards ?? data.source_plan?.shards ?? data.plan?.source_plan?.shards)
894
898
  };
895
899
  }
896
900
  async getCampaignBuildStatus(campaignBuildId) {
@@ -1082,6 +1086,9 @@ function recordOrNull(value) {
1082
1086
  function stringArray(value) {
1083
1087
  return Array.isArray(value) ? value.map((item) => String(item || "").trim()).filter(Boolean) : [];
1084
1088
  }
1089
+ function recordArray(value) {
1090
+ return Array.isArray(value) ? value.map(recordOrNull).filter((item) => Boolean(item)) : [];
1091
+ }
1085
1092
  function diagnosticMessages(value) {
1086
1093
  if (!Array.isArray(value)) return [];
1087
1094
  return value.map((item) => {
@@ -1243,7 +1250,8 @@ function buildArgs(request) {
1243
1250
  suppress_prior_campaign_ids: request.cacheReusePolicy.suppressPriorCampaignIds,
1244
1251
  suppress_prior_list_ids: request.cacheReusePolicy.suppressPriorListIds,
1245
1252
  uniqueness: request.cacheReusePolicy.uniqueness,
1246
- require_usage_metadata: request.cacheReusePolicy.requireUsageMetadata
1253
+ require_usage_metadata: request.cacheReusePolicy.requireUsageMetadata,
1254
+ verified_cache_ttl_days: request.cacheReusePolicy.verifiedCacheTtlDays
1247
1255
  };
1248
1256
  }
1249
1257
  if (request.brainPreflight) args.brain_preflight = request.brainPreflight;
@@ -1926,6 +1934,7 @@ var CampaignBuilderAgent = class {
1926
1934
  },
1927
1935
  brain_defaults: plan.buildRequest.brainDefaults,
1928
1936
  brain_preflight: plan.buildRequest.brainPreflight,
1937
+ learning_holdout: plan.buildRequest.learningHoldout,
1929
1938
  campaign_strategy_context: plan.buildRequest.campaignStrategyContext,
1930
1939
  delivery_risk_preflight: plan.buildRequest.deliveryRisk,
1931
1940
  delivery_risk: plan.buildRequest.deliveryRisk
@@ -1939,7 +1948,8 @@ var CampaignBuilderAgent = class {
1939
1948
  estimated_credits: plan.estimatedCredits
1940
1949
  },
1941
1950
  brain_delivery_risk_preflight: plan.buildRequest.deliveryRisk,
1942
- delivery_risk: plan.buildRequest.deliveryRisk
1951
+ delivery_risk: plan.buildRequest.deliveryRisk,
1952
+ learning_holdout: plan.buildRequest.learningHoldout
1943
1953
  },
1944
1954
  approval_required: plan.approvals.some((approval) => approval.blocking),
1945
1955
  actor_type: "agent",
@@ -3345,6 +3355,21 @@ function routesFromCustomerTools(tools) {
3345
3355
  }))
3346
3356
  );
3347
3357
  }
3358
+ function normalizeCampaignBuilderLearningHoldout(input, gtmCampaignId, campaignName) {
3359
+ const source = asRecord(input);
3360
+ if (source.enabled === false) return { enabled: false };
3361
+ const controlPercentage = numberOrUndefined2(source.control_percentage ?? source.controlPercentage);
3362
+ const minControlSize = numberOrUndefined2(source.min_control_size ?? source.minControlSize);
3363
+ const experimentKey = stringValue(source.experiment_key ?? source.experimentKey) ?? (gtmCampaignId ? `gtm_campaign:${gtmCampaignId}:learning_holdout` : `campaign_builder_agent:${hashString(campaignName)}:learning_holdout`);
3364
+ return {
3365
+ enabled: true,
3366
+ controlPercentage: controlPercentage ?? 0.2,
3367
+ minControlSize: minControlSize ?? 50,
3368
+ experimentKey,
3369
+ sourceCampaignId: stringValue(source.source_campaign_id ?? source.sourceCampaignId) ?? gtmCampaignId,
3370
+ primaryMetric: stringValue(source.primary_metric ?? source.primaryMetric) ?? "positive_reply_rate"
3371
+ };
3372
+ }
3348
3373
  function createBuildRequest(request, defaults, targetCount, campaignName, scopeBuildArgs, routes = [], memory, scopeBrainPreflight) {
3349
3374
  const buildRequest = {
3350
3375
  name: campaignName,
@@ -3377,6 +3402,11 @@ function createBuildRequest(request, defaults, targetCount, campaignName, scopeB
3377
3402
  };
3378
3403
  const scoped = asRecord(scopeBuildArgs);
3379
3404
  buildRequest.gtmCampaignId = request.gtmCampaignId ?? stringValue(scoped.gtm_campaign_id ?? scoped.gtmCampaignId);
3405
+ buildRequest.learningHoldout = normalizeCampaignBuilderLearningHoldout(
3406
+ request.learningHoldout ?? scoped.learning_holdout ?? scoped.learningHoldout,
3407
+ buildRequest.gtmCampaignId,
3408
+ campaignName
3409
+ );
3380
3410
  if (typeof scoped.name === "string") buildRequest.name = scoped.name;
3381
3411
  if (typeof scoped.prompt === "string") buildRequest.prompt = scoped.prompt;
3382
3412
  if (typeof scoped.target_count === "number") buildRequest.targetCount = scoped.target_count;
@@ -3451,6 +3481,7 @@ function createBrainPreflight(request, buildRequest, routes, memory) {
3451
3481
  target_icp: targetIcp,
3452
3482
  provider_chain: providerChain,
3453
3483
  lead_source: providerChain[0] ?? "signaliz",
3484
+ learning_holdout: buildRequest.learningHoldout,
3454
3485
  tool_sequence: [
3455
3486
  { tool: "gtm_brain_learning_cycle_plan", purpose: "Plan the Brain learning loop for this campaign.", arguments: learningArgs },
3456
3487
  { tool: "gtm_brain_seed_defaults", purpose: "Seed campaign defaults from workspace memory and safe aggregate patterns.", arguments: defaultsArgs },
@@ -4118,6 +4149,16 @@ function buildCampaignArgs(request) {
4118
4149
  if (request.dedupKeys) args.dedup_keys = request.dedupKeys;
4119
4150
  if (request.brainPreflight) args.brain_preflight = request.brainPreflight;
4120
4151
  if (request.brainDefaults) args.brain_defaults = request.brainDefaults;
4152
+ if (request.learningHoldout) {
4153
+ args.learning_holdout = compact({
4154
+ enabled: request.learningHoldout.enabled,
4155
+ control_percentage: request.learningHoldout.controlPercentage,
4156
+ min_control_size: request.learningHoldout.minControlSize,
4157
+ experiment_key: request.learningHoldout.experimentKey,
4158
+ source_campaign_id: request.learningHoldout.sourceCampaignId,
4159
+ primary_metric: request.learningHoldout.primaryMetric
4160
+ });
4161
+ }
4121
4162
  if (request.campaignStrategyContext) args.campaign_strategy_context = request.campaignStrategyContext;
4122
4163
  if (request.deliveryRisk) args.delivery_risk = request.deliveryRisk;
4123
4164
  if (request.icp) {
@@ -4214,6 +4255,7 @@ function buildExecutionPrepareArgs(request, dryRun, confirmSpend) {
4214
4255
  delivery_risk: buildArgs2.delivery_risk,
4215
4256
  dedup_keys: buildArgs2.dedup_keys,
4216
4257
  policy: buildArgs2.policy,
4258
+ learning_holdout: buildArgs2.learning_holdout,
4217
4259
  signals: buildArgs2.signals,
4218
4260
  qualification: buildArgs2.qualification,
4219
4261
  copy: buildArgs2.copy,
@@ -4248,10 +4290,145 @@ function mapBuildResult(data, dryRun) {
4248
4290
  targetLimitReason: data.target_limit_reason,
4249
4291
  maxSupportedTargetCount: data.max_supported_target_count,
4250
4292
  brainContext: data.brain_context ?? {},
4293
+ learningHoldout: data.learning_holdout ?? {},
4251
4294
  providerRoute: mapProviderRoute2(data)
4252
4295
  };
4253
4296
  }
4297
+ var CAMPAIGN_BUILDER_TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "canceled", "cancelled"]);
4298
+ var CAMPAIGN_BUILDER_BLOCKED_TERMINAL_STATUSES = /* @__PURE__ */ new Set(["failed", "canceled", "cancelled"]);
4299
+ function mapAgentCampaignPhaseHealth(value) {
4300
+ if (!Array.isArray(value)) return [];
4301
+ return value.map((phase) => {
4302
+ const record = asRecord(phase);
4303
+ return {
4304
+ phase: stringValue(record.phase) ?? "unknown",
4305
+ status: stringValue(record.status) ?? "unknown",
4306
+ recordsProcessed: numberOrNull2(record.records_processed ?? record.recordsProcessed),
4307
+ recordsSucceeded: numberOrNull2(record.records_succeeded ?? record.recordsSucceeded),
4308
+ recordsFailed: numberOrNull2(record.records_failed ?? record.recordsFailed),
4309
+ heartbeatAgeSeconds: numberOrNull2(record.heartbeat_age_seconds ?? record.heartbeatAgeSeconds ?? record.phase_age_seconds)
4310
+ };
4311
+ });
4312
+ }
4313
+ function deriveCampaignBuildTerminalState(input) {
4314
+ const status = input.status.toLowerCase();
4315
+ const heartbeatAgeSeconds = input.phaseAgeSeconds;
4316
+ const stale = input.staleRunningPhase === true;
4317
+ const safeWatchAction = input.triggerRunId ? `signaliz ops run-status ${input.triggerRunId} --watch` : "signaliz campaign-agent status <campaign_build_id>";
4318
+ if (CAMPAIGN_BUILDER_BLOCKED_TERMINAL_STATUSES.has(status)) {
4319
+ return {
4320
+ kind: "blocked",
4321
+ label: "Blocked terminal state",
4322
+ reviewable: true,
4323
+ terminal: true,
4324
+ stale: false,
4325
+ phase: input.phase,
4326
+ phaseStatus: input.phaseStatus,
4327
+ heartbeatAgeSeconds,
4328
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4329
+ terminalReason: input.terminalReason,
4330
+ staleReason: null,
4331
+ safeNextAction: input.nextAction || "Review errors and rerun only after the blocker is resolved."
4332
+ };
4333
+ }
4334
+ if (status === "pending_approval") {
4335
+ return {
4336
+ kind: "pending_approval",
4337
+ label: "Pending delivery approval",
4338
+ reviewable: true,
4339
+ terminal: false,
4340
+ stale: false,
4341
+ phase: input.phase,
4342
+ phaseStatus: input.phaseStatus,
4343
+ heartbeatAgeSeconds,
4344
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4345
+ terminalReason: input.terminalReason,
4346
+ staleReason: null,
4347
+ safeNextAction: input.approvalAction || input.nextAction || "Review rows and approve delivery only after the approval packet passes."
4348
+ };
4349
+ }
4350
+ if (CAMPAIGN_BUILDER_TERMINAL_STATUSES.has(status)) {
4351
+ return {
4352
+ kind: "terminal",
4353
+ label: "Terminal and reviewable",
4354
+ reviewable: true,
4355
+ terminal: true,
4356
+ stale: false,
4357
+ phase: input.phase,
4358
+ phaseStatus: input.phaseStatus,
4359
+ heartbeatAgeSeconds,
4360
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4361
+ terminalReason: input.terminalReason,
4362
+ staleReason: null,
4363
+ safeNextAction: input.nextAction || "Review rows, artifacts, scorecard, and approval locks before delivery."
4364
+ };
4365
+ }
4366
+ if (status === "running" || status === "queued" || status === "processing") {
4367
+ if (stale) {
4368
+ return {
4369
+ kind: "running_stale",
4370
+ label: "Running but stale",
4371
+ reviewable: false,
4372
+ terminal: false,
4373
+ stale: true,
4374
+ phase: input.phase,
4375
+ phaseStatus: input.phaseStatus,
4376
+ heartbeatAgeSeconds,
4377
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4378
+ terminalReason: null,
4379
+ staleReason: input.staleReason || "The active phase heartbeat is stale.",
4380
+ safeNextAction: input.nextAction || safeWatchAction
4381
+ };
4382
+ }
4383
+ return {
4384
+ kind: "running_healthy",
4385
+ label: "Running and healthy",
4386
+ reviewable: false,
4387
+ terminal: false,
4388
+ stale: false,
4389
+ phase: input.phase,
4390
+ phaseStatus: input.phaseStatus,
4391
+ heartbeatAgeSeconds,
4392
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4393
+ terminalReason: null,
4394
+ staleReason: null,
4395
+ safeNextAction: input.nextAction || safeWatchAction
4396
+ };
4397
+ }
4398
+ return {
4399
+ kind: "unknown",
4400
+ label: "Unknown build state",
4401
+ reviewable: false,
4402
+ terminal: false,
4403
+ stale,
4404
+ phase: input.phase,
4405
+ phaseStatus: input.phaseStatus,
4406
+ heartbeatAgeSeconds,
4407
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
4408
+ terminalReason: input.terminalReason,
4409
+ staleReason: input.staleReason,
4410
+ safeNextAction: input.nextAction || "Read campaign build status again before making a launch or delivery decision."
4411
+ };
4412
+ }
4254
4413
  function mapAgentCampaignBuildStatus(data) {
4414
+ const customerRowCounts = mapAgentCustomerRowCounts(data.customer_row_counts);
4415
+ const phaseHealth = mapAgentCampaignPhaseHealth(data.phases);
4416
+ const terminalReason = stringValue(data.terminal_reason) ?? customerRowCounts?.terminalReason ?? null;
4417
+ const staleReason = stringValue(data.stale_reason) ?? stringValue(asRecord(data.diagnostics).stale_reason) ?? null;
4418
+ const nextPollAfterSeconds = numberOrNull2(data.next_poll_after_seconds ?? data.nextPollAfterSeconds);
4419
+ const terminalState = deriveCampaignBuildTerminalState({
4420
+ status: stringValue(data.status) ?? "unknown",
4421
+ phase: stringValue(data.current_phase) ?? null,
4422
+ phaseStatus: stringValue(data.current_phase_status) ?? null,
4423
+ staleRunningPhase: data.stale_running_phase === true,
4424
+ phaseAgeSeconds: numberOrNull2(data.phase_age_seconds),
4425
+ nextPollAfterSeconds,
4426
+ terminalReason,
4427
+ staleReason,
4428
+ nextAction: formatAgentNextAction(data.next_action),
4429
+ approvalAction: formatAgentNextAction(data.approval_action),
4430
+ triggerRunId: stringValue(data.trigger_run_id) ?? null
4431
+ });
4255
4432
  return {
4256
4433
  campaignBuildId: stringValue(data.campaign_build_id) ?? "",
4257
4434
  campaignId: stringValue(data.campaign_id) ?? null,
@@ -4269,12 +4446,18 @@ function mapAgentCampaignBuildStatus(data) {
4269
4446
  errors: diagnosticMessages2(data.errors),
4270
4447
  artifactCount: numberOrUndefined2(data.artifact_count) ?? 0,
4271
4448
  artifactDownloads: Array.isArray(data.artifact_downloads) ? data.artifact_downloads.map(mapAgentCampaignArtifactDownload) : [],
4272
- customerRowCounts: mapAgentCustomerRowCounts(data.customer_row_counts),
4449
+ customerRowCounts,
4273
4450
  phaseAttemptTotals: nullableRecord(data.phase_attempt_totals) ?? void 0,
4274
4451
  providerRoute: mapProviderRoute2(data),
4275
4452
  triggerRunId: stringValue(data.trigger_run_id) ?? null,
4276
4453
  staleRunningPhase: data.stale_running_phase === true,
4277
4454
  phaseAgeSeconds: numberOrNull2(data.phase_age_seconds),
4455
+ nextPollAfterSeconds,
4456
+ terminalReason,
4457
+ staleReason,
4458
+ safeNextAction: terminalState.safeNextAction,
4459
+ terminalState,
4460
+ phaseHealth,
4278
4461
  diagnostics: nullableRecord(data.diagnostics) ?? {},
4279
4462
  nextAction: formatAgentNextAction(data.next_action),
4280
4463
  approvalAction: formatAgentNextAction(data.approval_action),
@@ -4362,67 +4545,285 @@ function mapAgentCampaignArtifact(data) {
4362
4545
  createdAt: stringValue(data.created_at) ?? ""
4363
4546
  };
4364
4547
  }
4548
+ var NORTH_STAR_BANNED_COPY_PHRASES = [
4549
+ "i have been following",
4550
+ "i saw that",
4551
+ "would you be opposed",
4552
+ "unlock new opportunities",
4553
+ "significant success",
4554
+ "we understand",
4555
+ "hope this finds you well"
4556
+ ];
4557
+ function campaignReviewGate(id, label, status, detail) {
4558
+ return {
4559
+ id,
4560
+ label,
4561
+ status,
4562
+ passed: status !== "blocked",
4563
+ detail
4564
+ };
4565
+ }
4566
+ function northStarMetric(id, label, status, detail, value, target) {
4567
+ return {
4568
+ id,
4569
+ label,
4570
+ status,
4571
+ detail,
4572
+ ...value !== void 0 ? { value } : {},
4573
+ ...target !== void 0 ? { target } : {}
4574
+ };
4575
+ }
4576
+ function createCampaignBuilderNorthStarScorecard(status, rows, artifacts, terminalState) {
4577
+ if (!terminalState.reviewable) {
4578
+ const pendingMetric = northStarMetric(
4579
+ "terminal_state",
4580
+ "Terminal-state contract",
4581
+ terminalState.kind === "running_stale" ? "blocked" : "pending",
4582
+ 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.`,
4583
+ terminalState.kind,
4584
+ "terminal or pending_approval"
4585
+ );
4586
+ return {
4587
+ version: "campaign-builder-north-star.v1",
4588
+ status: pendingMetric.status,
4589
+ score: null,
4590
+ moatState: "scaffolding_ready",
4591
+ metrics: [pendingMetric],
4592
+ blockers: pendingMetric.status === "blocked" ? [pendingMetric.detail] : [],
4593
+ warnings: pendingMetric.status === "pending" ? [pendingMetric.detail] : [],
4594
+ nextAction: terminalState.safeNextAction
4595
+ };
4596
+ }
4597
+ const sampledRows = rows.rows.length;
4598
+ const rowData = rows.rows.map((row) => asRecord(row.data));
4599
+ const requestedTarget = status.customerRowCounts?.requestedTarget ?? status.recordsTotal ?? rows.count;
4600
+ const finalRows = status.customerRowCounts?.deliveredRows ?? status.customerRowCounts?.qaReadyRows ?? status.customerRowCounts?.copiedRows ?? status.customerRowCounts?.acceptedRows ?? rows.count;
4601
+ const rowsWithEmail = rows.rows.filter(campaignReviewRowHasEmail).length;
4602
+ const verifiedEmailRows = rowData.filter(campaignReviewDataHasVerifiedEmail).length;
4603
+ const uniqueEmails = new Set(rowData.map(campaignReviewDataEmail).filter(Boolean)).size;
4604
+ const uniqueDomains = new Set(rowData.map(campaignReviewDataDomain).filter(Boolean)).size;
4605
+ const copyReadyRows = rows.rows.filter(campaignReviewRowHasCopy).length;
4606
+ const copyQaIssuesByRow = rowData.map(campaignReviewDataCopyQaIssues).filter((issues) => issues.length > 0);
4607
+ const copyQaIssueCounts = countCampaignReviewIssues(copyQaIssuesByRow);
4608
+ const signalBackedRows = rowData.filter(campaignReviewDataHasSignalBasis).length;
4609
+ const noSupportedSignalRows = rowData.filter(campaignReviewDataHasNoSupportedSignal).length;
4610
+ const unsupportedSignalClaims = rowData.filter(campaignReviewDataHasUnsupportedSignalClaim).length;
4611
+ const approvalLockedRows = rowData.filter(campaignReviewDataHasApprovalLock).length;
4612
+ const holdoutRows = rowData.filter(campaignReviewDataHasHoldoutAttribution).length;
4613
+ const artifactCount = artifacts.length || status.artifactCount || 0;
4614
+ const deliveredRows = status.customerRowCounts?.deliveredRows ?? 0;
4615
+ const feedbackReady = campaignReviewStatusHasFeedbackReadiness(status);
4616
+ const feedbackRequiredForApproval = terminalState.kind === "pending_approval";
4617
+ const moat = campaignReviewMoatState(status, holdoutRows);
4618
+ const metrics = [
4619
+ northStarMetric(
4620
+ "terminal_state",
4621
+ "Terminal-state contract",
4622
+ terminalState.kind === "blocked" ? "blocked" : "pass",
4623
+ terminalState.label,
4624
+ terminalState.kind,
4625
+ "terminal or pending_approval"
4626
+ ),
4627
+ northStarMetric(
4628
+ "target_fill",
4629
+ "Target fill",
4630
+ requestedTarget && finalRows < requestedTarget ? "review" : "pass",
4631
+ requestedTarget ? `${finalRows || 0}/${requestedTarget} final or reviewable row(s) are accounted for.` : "No requested target was reported; using sampled rows for review.",
4632
+ finalRows || sampledRows,
4633
+ requestedTarget || sampledRows
4634
+ ),
4635
+ northStarMetric(
4636
+ "unique_emails",
4637
+ "Unique emails",
4638
+ sampledRows === 0 ? "blocked" : uniqueEmails === rowsWithEmail ? "pass" : "blocked",
4639
+ sampledRows === 0 ? "No sampled rows were available for email uniqueness review." : `${uniqueEmails}/${rowsWithEmail} sampled email(s) are unique.`,
4640
+ uniqueEmails,
4641
+ rowsWithEmail
4642
+ ),
4643
+ northStarMetric(
4644
+ "unique_domains",
4645
+ "Unique domains",
4646
+ sampledRows === 0 ? "blocked" : uniqueDomains === sampledRows ? "pass" : "review",
4647
+ sampledRows === 0 ? "No sampled rows were available for domain uniqueness review." : `${uniqueDomains}/${sampledRows} sampled company domain(s) are unique.`,
4648
+ uniqueDomains,
4649
+ sampledRows
4650
+ ),
4651
+ northStarMetric(
4652
+ "verified_email_rows",
4653
+ "Verified email rows",
4654
+ sampledRows === 0 ? "blocked" : verifiedEmailRows === sampledRows ? "pass" : rowsWithEmail === sampledRows ? "review" : "blocked",
4655
+ sampledRows === 0 ? "No sampled rows were available for email verification review." : `${verifiedEmailRows}/${sampledRows} sampled row(s) carry verified-email evidence.`,
4656
+ verifiedEmailRows,
4657
+ sampledRows
4658
+ ),
4659
+ northStarMetric(
4660
+ "copy_completeness",
4661
+ "Copy completeness",
4662
+ sampledRows === 0 ? "blocked" : copyReadyRows === sampledRows ? "pass" : "blocked",
4663
+ sampledRows === 0 ? "No sampled rows were available for copy review." : `${copyReadyRows}/${sampledRows} sampled row(s) include generated copy.`,
4664
+ copyReadyRows,
4665
+ sampledRows
4666
+ ),
4667
+ northStarMetric(
4668
+ "copy_quality",
4669
+ "Copy QA",
4670
+ copyQaIssuesByRow.length === 0 ? "pass" : "blocked",
4671
+ copyQaIssuesByRow.length === 0 ? "No sampled copy QA blockers were detected." : `${copyQaIssuesByRow.length}/${sampledRows} sampled row(s) have copy QA issue(s): ${formatCampaignReviewIssueCounts(copyQaIssueCounts)}.`,
4672
+ copyQaIssuesByRow.length,
4673
+ 0
4674
+ ),
4675
+ northStarMetric(
4676
+ "signal_grounding",
4677
+ "Signal grounding",
4678
+ unsupportedSignalClaims > 0 ? "blocked" : signalBackedRows > 0 ? "pass" : "review",
4679
+ 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.",
4680
+ signalBackedRows,
4681
+ sampledRows
4682
+ ),
4683
+ northStarMetric(
4684
+ "approval_locks",
4685
+ "Approval locks",
4686
+ approvalLockedRows > 0 || deliveredRows === 0 ? "pass" : "review",
4687
+ 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.",
4688
+ approvalLockedRows,
4689
+ sampledRows
4690
+ ),
4691
+ northStarMetric(
4692
+ "export_send_safety",
4693
+ "Export/send safety",
4694
+ deliveredRows > 0 && status.status !== "completed" ? "blocked" : "pass",
4695
+ deliveredRows > 0 ? `${deliveredRows} row(s) are delivered; status is ${status.status}.` : "No delivered rows are reported before approval.",
4696
+ deliveredRows,
4697
+ 0
4698
+ ),
4699
+ northStarMetric(
4700
+ "holdout_attribution",
4701
+ "Holdout attribution",
4702
+ holdoutRows > 0 ? "pass" : "review",
4703
+ holdoutRows > 0 ? `${holdoutRows}/${sampledRows} sampled row(s) preserve holdout attribution.` : "No holdout attribution was visible in the sampled rows.",
4704
+ holdoutRows,
4705
+ sampledRows
4706
+ ),
4707
+ northStarMetric(
4708
+ "feedback_readiness",
4709
+ "Feedback readiness",
4710
+ feedbackReady ? "pass" : feedbackRequiredForApproval ? "blocked" : "review",
4711
+ 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.",
4712
+ feedbackReady,
4713
+ true
4714
+ ),
4715
+ northStarMetric(
4716
+ "causal_moat",
4717
+ "Causal moat",
4718
+ moat.state === "proved" ? "pass" : "review",
4719
+ moat.detail,
4720
+ moat.state,
4721
+ "proved"
4722
+ ),
4723
+ northStarMetric(
4724
+ "artifact_readback",
4725
+ "Artifact readback",
4726
+ artifactCount > 0 ? "pass" : "review",
4727
+ artifactCount > 0 ? `${artifactCount} artifact(s) are available for review.` : "No review artifact is available yet.",
4728
+ artifactCount,
4729
+ 1
4730
+ )
4731
+ ];
4732
+ const gradedMetrics = metrics.filter((metric) => metric.status !== "pending");
4733
+ const score = gradedMetrics.length === 0 ? null : Math.round(gradedMetrics.filter((metric) => metric.status === "pass").length / gradedMetrics.length * 100);
4734
+ const blockers = metrics.filter((metric) => metric.status === "blocked").map((metric) => metric.detail);
4735
+ const warnings = metrics.filter((metric) => metric.status === "review").map((metric) => metric.detail);
4736
+ const scorecardStatus = blockers.length > 0 ? "blocked" : warnings.length > 0 ? "review" : "pass";
4737
+ return {
4738
+ version: "campaign-builder-north-star.v1",
4739
+ status: scorecardStatus,
4740
+ score,
4741
+ moatState: moat.state,
4742
+ metrics,
4743
+ blockers,
4744
+ warnings,
4745
+ nextAction: scorecardStatus === "blocked" ? "Fix blocked North Star gates before approval or delivery." : terminalState.safeNextAction
4746
+ };
4747
+ }
4365
4748
  function createCampaignBuilderBuildReview(status, rows, artifacts) {
4749
+ const terminalState = status.terminalState ?? deriveCampaignBuildTerminalState({
4750
+ status: status.status,
4751
+ phase: status.currentPhase,
4752
+ phaseStatus: status.currentPhaseStatus,
4753
+ staleRunningPhase: status.staleRunningPhase === true,
4754
+ phaseAgeSeconds: status.phaseAgeSeconds ?? null,
4755
+ nextPollAfterSeconds: status.nextPollAfterSeconds ?? null,
4756
+ terminalReason: status.terminalReason ?? status.customerRowCounts?.terminalReason ?? null,
4757
+ staleReason: status.staleReason ?? null,
4758
+ nextAction: status.nextAction,
4759
+ approvalAction: status.approvalAction,
4760
+ triggerRunId: status.triggerRunId ?? null
4761
+ });
4366
4762
  const sampledRows = rows.rows.length;
4367
4763
  const availableRows = rows.count || sampledRows;
4368
4764
  const qualifiedRows = rows.rows.filter((row) => row.qualified !== false).length;
4369
4765
  const rowsWithEmail = rows.rows.filter(campaignReviewRowHasEmail).length;
4370
4766
  const copyReadyRows = rows.rows.filter(campaignReviewRowHasCopy).length;
4371
4767
  const artifactCount = artifacts.length || status.artifactCount || 0;
4768
+ const northStarScorecard = createCampaignBuilderNorthStarScorecard(status, rows, artifacts, terminalState);
4372
4769
  const gates = [
4373
- {
4374
- id: "build_completed",
4375
- label: "Build completed",
4376
- passed: status.status === "completed",
4377
- detail: status.status === "completed" ? "The campaign build is completed." : `The campaign build is ${status.status}; review again after completion.`
4378
- },
4379
- {
4380
- id: "rows_available",
4381
- label: "Rows available",
4382
- passed: sampledRows > 0,
4383
- detail: sampledRows > 0 ? `${sampledRows} sampled row(s) are available for review.` : "No rows were returned for review."
4384
- },
4385
- {
4386
- id: "qualified_sample",
4387
- label: "Sample rows qualified",
4388
- passed: sampledRows > 0 && qualifiedRows === sampledRows,
4389
- detail: sampledRows > 0 ? `${qualifiedRows}/${sampledRows} sampled row(s) are marked qualified.` : "Qualification could not be checked without sampled rows."
4390
- },
4391
- {
4392
- id: "email_coverage",
4393
- label: "Email coverage",
4394
- passed: sampledRows > 0 && rowsWithEmail === sampledRows,
4395
- detail: sampledRows > 0 ? `${rowsWithEmail}/${sampledRows} sampled row(s) include an email.` : "Email coverage could not be checked without sampled rows."
4396
- },
4397
- {
4398
- id: "artifact_available",
4399
- label: "Artifact available",
4400
- passed: artifactCount > 0,
4401
- detail: artifactCount > 0 ? `${artifactCount} artifact(s) are available.` : "No delivery artifact is available yet."
4402
- },
4403
- {
4404
- id: "no_failed_rows",
4405
- label: "No failed rows",
4406
- passed: status.recordsFailed === 0,
4407
- detail: status.recordsFailed === 0 ? "The build reports zero failed rows." : `${status.recordsFailed} row(s) failed during the build.`
4408
- }
4770
+ campaignReviewGate(
4771
+ "build_reviewable",
4772
+ "Build reviewable",
4773
+ terminalState.kind === "running_healthy" ? "pending" : terminalState.kind === "running_stale" || terminalState.kind === "blocked" ? "blocked" : "pass",
4774
+ terminalState.label
4775
+ ),
4776
+ campaignReviewGate(
4777
+ "rows_available",
4778
+ "Rows available",
4779
+ sampledRows > 0 ? "pass" : terminalState.reviewable ? "blocked" : "pending",
4780
+ 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."
4781
+ ),
4782
+ campaignReviewGate(
4783
+ "qualified_sample",
4784
+ "Sample rows qualified",
4785
+ sampledRows === 0 ? terminalState.reviewable ? "blocked" : "pending" : qualifiedRows === sampledRows ? "pass" : "blocked",
4786
+ sampledRows > 0 ? `${qualifiedRows}/${sampledRows} sampled row(s) are marked qualified.` : "Qualification could not be checked without sampled rows."
4787
+ ),
4788
+ campaignReviewGate(
4789
+ "email_coverage",
4790
+ "Email coverage",
4791
+ sampledRows === 0 ? terminalState.reviewable ? "blocked" : "pending" : rowsWithEmail === sampledRows ? "pass" : "blocked",
4792
+ sampledRows > 0 ? `${rowsWithEmail}/${sampledRows} sampled row(s) include an email.` : "Email coverage could not be checked without sampled rows."
4793
+ ),
4794
+ campaignReviewGate(
4795
+ "artifact_available",
4796
+ "Artifact available",
4797
+ artifactCount > 0 ? "pass" : terminalState.reviewable ? "review" : "pending",
4798
+ artifactCount > 0 ? `${artifactCount} artifact(s) are available.` : "No delivery artifact is available yet."
4799
+ ),
4800
+ campaignReviewGate(
4801
+ "no_failed_rows",
4802
+ "No failed rows",
4803
+ status.recordsFailed === 0 ? "pass" : "blocked",
4804
+ status.recordsFailed === 0 ? "The build reports zero failed rows." : `${status.recordsFailed} row(s) failed during the build.`
4805
+ )
4409
4806
  ];
4410
- const blockedReasons = gates.filter((gate) => !gate.passed).map((gate) => gate.detail);
4807
+ const blockedReasons = gates.filter((gate) => gate.status === "blocked" || !gate.passed).map((gate) => gate.detail);
4411
4808
  const warnings = [
4412
4809
  rows.hasMore ? "This review is based on a row sample; page through remaining rows before final approval." : void 0,
4413
4810
  sampledRows > 0 && copyReadyRows < sampledRows ? `${copyReadyRows}/${sampledRows} sampled row(s) include generated copy.` : void 0,
4414
4811
  ...status.warnings
4415
4812
  ].filter((item) => Boolean(item));
4416
- const readyForDeliveryApproval = blockedReasons.length === 0;
4813
+ const readyForDeliveryApproval = terminalState.reviewable && blockedReasons.length === 0 && northStarScorecard.status !== "blocked";
4417
4814
  return {
4418
4815
  campaignBuildId: status.campaignBuildId || rows.campaignBuildId,
4419
4816
  status,
4420
4817
  rows,
4421
4818
  artifacts,
4422
4819
  gates,
4820
+ northStarScorecard,
4423
4821
  summary: {
4424
4822
  status: status.status,
4425
4823
  phase: status.currentPhase,
4824
+ terminalState: terminalState.kind,
4825
+ scorecardStatus: northStarScorecard.status,
4826
+ scorecardScore: northStarScorecard.score,
4426
4827
  sampledRows,
4427
4828
  availableRows,
4428
4829
  qualifiedRows,
@@ -4476,6 +4877,184 @@ function campaignReviewRowHasCopy(row) {
4476
4877
  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)
4477
4878
  );
4478
4879
  }
4880
+ function campaignReviewDataEmail(data) {
4881
+ return stringValue(data.email) || stringValue(data.work_email) || stringValue(data.workEmail) || stringValue(asRecord(data.contact).email);
4882
+ }
4883
+ function campaignReviewDataDomain(data) {
4884
+ const domain = stringValue(data.company_domain) || stringValue(data.companyDomain) || stringValue(asRecord(data.company).domain);
4885
+ return domain?.toLowerCase();
4886
+ }
4887
+ function campaignReviewDataHasVerifiedEmail(data) {
4888
+ const status = String(data.email_status ?? data.emailStatus ?? asRecord(data.contact).email_status ?? "").toLowerCase();
4889
+ return data.email_verified === true || data.emailVerified === true || asRecord(data.contact).email_verified === true || ["valid", "validated", "verified"].includes(status);
4890
+ }
4891
+ function campaignReviewDataCopyParts(data) {
4892
+ const copy = asRecord(data.copy);
4893
+ const rawCopy = asRecord(data.raw_copy ?? data.rawCopy);
4894
+ return [
4895
+ stringValue(data.subject),
4896
+ stringValue(data.subject_line),
4897
+ stringValue(data.opener),
4898
+ stringValue(data.body),
4899
+ stringValue(data.cta),
4900
+ stringValue(copy.subject),
4901
+ stringValue(copy.opener),
4902
+ stringValue(copy.body),
4903
+ stringValue(copy.cta),
4904
+ stringValue(rawCopy.subject),
4905
+ stringValue(rawCopy.opener),
4906
+ stringValue(rawCopy.body),
4907
+ stringValue(rawCopy.cta)
4908
+ ].filter((part) => Boolean(part));
4909
+ }
4910
+ function campaignReviewDataHasBadGreeting(data) {
4911
+ return campaignReviewDataCopyParts(data).some(
4912
+ (part) => /\bhi\s*(?:,|\{\{|\[)|\bhello\s*(?:,|\{\{|\[)/i.test(part) || /\b(first_name|firstname|name)\b/i.test(part)
4913
+ );
4914
+ }
4915
+ function campaignReviewDataHasBannedPhrase(data) {
4916
+ const text = campaignReviewDataCopyParts(data).join(" ").toLowerCase();
4917
+ return NORTH_STAR_BANNED_COPY_PHRASES.some((phrase) => text.includes(phrase));
4918
+ }
4919
+ function campaignReviewDataSubject(data) {
4920
+ const copy = asRecord(data.copy);
4921
+ const rawCopy = asRecord(data.raw_copy ?? data.rawCopy);
4922
+ return stringValue(data.subject) || stringValue(data.subject_line) || stringValue(data.subjectLine) || stringValue(copy.subject) || stringValue(rawCopy.subject);
4923
+ }
4924
+ function campaignReviewDataCta(data) {
4925
+ const copy = asRecord(data.copy);
4926
+ const rawCopy = asRecord(data.raw_copy ?? data.rawCopy);
4927
+ return stringValue(data.cta) || stringValue(copy.cta) || stringValue(rawCopy.cta);
4928
+ }
4929
+ function countCampaignReviewIssues(issueRows) {
4930
+ return issueRows.flat().reduce((acc, issue) => {
4931
+ acc[issue] = (acc[issue] || 0) + 1;
4932
+ return acc;
4933
+ }, {});
4934
+ }
4935
+ function formatCampaignReviewIssueCounts(counts) {
4936
+ const entries = Object.entries(counts);
4937
+ return entries.length > 0 ? entries.map(([issue, count]) => `${issue}=${count}`).join(", ") : "none";
4938
+ }
4939
+ function campaignReviewDataCopyQaIssues(data) {
4940
+ const issues = /* @__PURE__ */ new Set();
4941
+ const copyParts = campaignReviewDataCopyParts(data);
4942
+ if (copyParts.length === 0) return [];
4943
+ if (campaignReviewDataHasBadGreeting(data)) issues.add("bad_greeting");
4944
+ if (campaignReviewDataHasBannedPhrase(data)) issues.add("banned_or_vague_phrase");
4945
+ if (campaignReviewDataHasUnsupportedSignalClaim(data)) issues.add("unsupported_signal_claim");
4946
+ if (campaignReviewDataPersonalizationBasis(data).length === 0) issues.add("missing_personalization_basis");
4947
+ if (!campaignReviewDataCta(data)) issues.add("missing_cta");
4948
+ const subject = campaignReviewDataSubject(data);
4949
+ if (subject && subject.length > 70) issues.add("overlong_subject");
4950
+ return [...issues];
4951
+ }
4952
+ function campaignReviewDataPersonalizationBasis(data) {
4953
+ const rawCopy = asRecord(data.raw_copy ?? data.rawCopy);
4954
+ const copy = asRecord(data.copy);
4955
+ const metadata = asRecord(data.metadata);
4956
+ const signalEvidence = asRecord(data.signal_evidence ?? metadata.signal_evidence ?? metadata.signalEvidence);
4957
+ return [
4958
+ ...arrayOfStrings(data.personalization_basis) ?? [],
4959
+ ...arrayOfStrings(data.personalizationBasis) ?? [],
4960
+ ...arrayOfStrings(copy.personalization_basis) ?? [],
4961
+ ...arrayOfStrings(rawCopy.personalization_basis) ?? [],
4962
+ ...arrayOfStrings(metadata.personalization_basis) ?? [],
4963
+ ...arrayOfStrings(metadata.personalizationBasis) ?? [],
4964
+ ...arrayOfStrings(signalEvidence.personalization_basis) ?? []
4965
+ ];
4966
+ }
4967
+ function campaignReviewDataHasNoSupportedSignal(data) {
4968
+ const metadata = asRecord(data.metadata);
4969
+ const copy = asRecord(data.copy);
4970
+ const state = [
4971
+ data.copy_signal_state,
4972
+ data.signal_basis,
4973
+ data.signal_type,
4974
+ metadata.copy_signal_state,
4975
+ metadata.signal_basis,
4976
+ metadata.signal_type,
4977
+ copy.copy_signal_state
4978
+ ].map((value) => stringValue(value)?.toLowerCase()).filter(Boolean);
4979
+ return data.no_supported_signal === true || metadata.no_supported_signal === true || state.includes("no_supported_signal");
4980
+ }
4981
+ function campaignReviewDataHasSignalBasis(data) {
4982
+ if (campaignReviewDataHasNoSupportedSignal(data)) return false;
4983
+ const metadata = asRecord(data.metadata);
4984
+ const signal = asRecord(data.signal);
4985
+ const signalEvidence = asRecord(data.signal_evidence ?? metadata.signal_evidence ?? metadata.signalEvidence);
4986
+ const basis = campaignReviewDataPersonalizationBasis(data);
4987
+ return Boolean(
4988
+ 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))
4989
+ );
4990
+ }
4991
+ function campaignReviewDataHasUnsupportedSignalClaim(data) {
4992
+ const copyText = campaignReviewDataCopyParts(data).join(" ").toLowerCase();
4993
+ const claimsSignal = /\b(hiring|funding|raised|launched|expanding|appointed|announced|recent news|new role|job posting)\b/.test(copyText);
4994
+ return claimsSignal && !campaignReviewDataHasSignalBasis(data);
4995
+ }
4996
+ function campaignReviewDataHasApprovalLock(data) {
4997
+ const metadata = asRecord(data.fit_metadata ?? data.metadata);
4998
+ const approval = String(data.approval_status ?? data.delivery_approval_status ?? metadata.approval_status ?? "").toLowerCase();
4999
+ const exportStatus = String(data.export_status ?? data.exportStatus ?? "").toLowerCase();
5000
+ 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);
5001
+ }
5002
+ function campaignReviewDataHasHoldoutAttribution(data) {
5003
+ const fitMetadata = asRecord(data.fit_metadata ?? data.fitMetadata);
5004
+ const metadata = asRecord(data.metadata);
5005
+ const holdout = asRecord(
5006
+ fitMetadata.learning_holdout ?? fitMetadata.learningHoldout ?? metadata.learning_holdout ?? metadata.learningHoldout ?? data.learning_holdout ?? data.learningHoldout
5007
+ );
5008
+ const experimentKey = stringValue(holdout.experiment_key ?? holdout.experimentKey ?? data.experiment_key ?? data.experimentKey);
5009
+ const cohort = stringValue(holdout.cohort_role ?? holdout.cohortRole ?? data.cohort_role ?? data.learning_cohort ?? data.learningCohort);
5010
+ const brainApplied = holdout.brain_applied ?? holdout.brainApplied ?? data.brain_applied ?? data.signaliz_brain_applied;
5011
+ return Boolean(experimentKey && cohort && brainApplied !== void 0);
5012
+ }
5013
+ function campaignReviewStatusHasFeedbackReadiness(status) {
5014
+ const diagnostics = asRecord(status.diagnostics);
5015
+ const feedbackReadiness = asRecord(diagnostics.feedback_readiness ?? diagnostics.feedbackReadiness);
5016
+ const feedbackSubstrate = asRecord(diagnostics.feedback_substrate ?? diagnostics.feedbackSubstrate);
5017
+ const evidenceCounts = asRecord(diagnostics.evidence_counts ?? diagnostics.evidenceCounts);
5018
+ return feedbackReadiness.ready === true || feedbackReadiness.webhook_configured === true || feedbackSubstrate.stage === "ready" || Number(evidenceCounts.feedback_events ?? 0) > 0;
5019
+ }
5020
+ function campaignReviewMoatState(status, holdoutRows) {
5021
+ const diagnostics = asRecord(status.diagnostics);
5022
+ const evidenceCounts = asRecord(diagnostics.evidence_counts ?? diagnostics.evidenceCounts);
5023
+ const embeddedHoldout = asRecord(diagnostics.embedded_holdout ?? diagnostics.embeddedHoldout);
5024
+ const holdoutLift = asRecord(diagnostics.holdout_lift ?? diagnostics.holdoutLift ?? diagnostics.holdout_measurement ?? diagnostics.holdoutMeasurement);
5025
+ const controlSample = Math.max(
5026
+ Number(evidenceCounts.holdout_control_sample_size ?? 0),
5027
+ Number(evidenceCounts.embedded_holdout_control_sample ?? 0),
5028
+ Number(embeddedHoldout.control_sample_size ?? 0)
5029
+ );
5030
+ const learnedSample = Math.max(
5031
+ Number(evidenceCounts.holdout_learned_sample_size ?? 0),
5032
+ Number(evidenceCounts.embedded_holdout_learned_sample ?? 0),
5033
+ Number(embeddedHoldout.learned_sample_size ?? 0)
5034
+ );
5035
+ const feedbackEvents = Math.max(
5036
+ Number(evidenceCounts.feedback_events ?? 0),
5037
+ Number(evidenceCounts.campaign_build_feedback_events ?? 0),
5038
+ Number(evidenceCounts.delivery_holdout_sent_outcomes ?? 0)
5039
+ );
5040
+ const proved = diagnostics.closed_loop_proof_ready === true || holdoutLift.honest_measurement === true && holdoutLift.positive_lift === true;
5041
+ if (proved) {
5042
+ return {
5043
+ state: "proved",
5044
+ detail: "Real attributed feedback shows positive lift for explicit control and learned cohorts."
5045
+ };
5046
+ }
5047
+ if (controlSample > 0 && learnedSample > 0 && feedbackEvents > 0) {
5048
+ return {
5049
+ state: "moved_needs_lift_volume",
5050
+ detail: `Control and learned cohorts have attributed feedback (${controlSample} control, ${learnedSample} learned, ${feedbackEvents} feedback event(s)); keep collecting lift volume before claiming proof.`
5051
+ };
5052
+ }
5053
+ return {
5054
+ state: "scaffolding_ready",
5055
+ 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."
5056
+ };
5057
+ }
4479
5058
  function createCampaignBuilderReadinessLane(route, plan) {
4480
5059
  const builtIn = campaignBuilderBuiltInForRoute(route);
4481
5060
  const customRoute = route.provider !== "signaliz" && route.provider !== "csv" && builtIn === void 0;
@@ -7691,6 +8270,16 @@ var GtmKernel = class {
7691
8270
  min_privacy_k: input.minPrivacyK
7692
8271
  });
7693
8272
  }
8273
+ /** Queue a dry-run-first bridge from imported historical feedback to attribution-only Campaign Build surrogates. */
8274
+ async runHistoricalCampaignBuildBridge(input = {}) {
8275
+ return this.callMcp("gtm_historical_campaign_build_bridge_run", {
8276
+ dry_run: input.dryRun,
8277
+ write_approved: input.writeApproved,
8278
+ max_events: input.maxEvents,
8279
+ max_campaigns: input.maxCampaigns,
8280
+ min_feedback_events: input.minFeedbackEvents
8281
+ });
8282
+ }
7694
8283
  /** Prepare a provider-agnostic feedback webhook URL for Smartlead, HeyReach, Airbyte, or custom sender events. */
7695
8284
  async prepareFeedbackWebhook(input) {
7696
8285
  return this.callMcp("gtm_feedback_webhook_prepare", {