@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/cli.js CHANGED
@@ -881,6 +881,9 @@ function recordOrNull(value) {
881
881
  function stringArray(value) {
882
882
  return Array.isArray(value) ? value.map((item) => String(item || "").trim()).filter(Boolean) : [];
883
883
  }
884
+ function recordArray(value) {
885
+ return Array.isArray(value) ? value.map(recordOrNull).filter((item) => Boolean(item)) : [];
886
+ }
884
887
  function diagnosticMessages(value) {
885
888
  if (!Array.isArray(value)) return [];
886
889
  return value.map((item) => {
@@ -1042,7 +1045,8 @@ function buildArgs(request) {
1042
1045
  suppress_prior_campaign_ids: request.cacheReusePolicy.suppressPriorCampaignIds,
1043
1046
  suppress_prior_list_ids: request.cacheReusePolicy.suppressPriorListIds,
1044
1047
  uniqueness: request.cacheReusePolicy.uniqueness,
1045
- require_usage_metadata: request.cacheReusePolicy.requireUsageMetadata
1048
+ require_usage_metadata: request.cacheReusePolicy.requireUsageMetadata,
1049
+ verified_cache_ttl_days: request.cacheReusePolicy.verifiedCacheTtlDays
1046
1050
  };
1047
1051
  }
1048
1052
  if (request.brainPreflight) args.brain_preflight = request.brainPreflight;
@@ -1247,7 +1251,11 @@ var init_campaigns = __esm({
1247
1251
  maxSupportedTargetCount: data.max_supported_target_count,
1248
1252
  brainContext: data.brain_context ?? {},
1249
1253
  learningHoldout: data.learning_holdout ?? {},
1250
- providerRoute: mapProviderRoute(data)
1254
+ providerRoute: mapProviderRoute(data),
1255
+ effectiveSourceRouting: recordOrNull(data.effective_source_routing ?? data.source_routing),
1256
+ sourcePlan: recordOrNull(data.source_plan ?? data.plan?.source_plan),
1257
+ sourceRoutes: recordArray(data.source_routes ?? data.source_plan?.routes ?? data.plan?.source_plan?.routes),
1258
+ sourceShards: recordArray(data.source_shards ?? data.source_plan?.shards ?? data.plan?.source_plan?.shards)
1251
1259
  };
1252
1260
  }
1253
1261
  async getCampaignBuildStatus(campaignBuildId) {
@@ -2446,6 +2454,21 @@ function routesFromCustomerTools(tools) {
2446
2454
  }))
2447
2455
  );
2448
2456
  }
2457
+ function normalizeCampaignBuilderLearningHoldout(input, gtmCampaignId, campaignName) {
2458
+ const source = asRecord(input);
2459
+ if (source.enabled === false) return { enabled: false };
2460
+ const controlPercentage = numberOrUndefined2(source.control_percentage ?? source.controlPercentage);
2461
+ const minControlSize = numberOrUndefined2(source.min_control_size ?? source.minControlSize);
2462
+ const experimentKey = stringValue(source.experiment_key ?? source.experimentKey) ?? (gtmCampaignId ? `gtm_campaign:${gtmCampaignId}:learning_holdout` : `campaign_builder_agent:${hashString(campaignName)}:learning_holdout`);
2463
+ return {
2464
+ enabled: true,
2465
+ controlPercentage: controlPercentage ?? 0.2,
2466
+ minControlSize: minControlSize ?? 50,
2467
+ experimentKey,
2468
+ sourceCampaignId: stringValue(source.source_campaign_id ?? source.sourceCampaignId) ?? gtmCampaignId,
2469
+ primaryMetric: stringValue(source.primary_metric ?? source.primaryMetric) ?? "positive_reply_rate"
2470
+ };
2471
+ }
2449
2472
  function createBuildRequest(request, defaults, targetCount, campaignName, scopeBuildArgs, routes = [], memory, scopeBrainPreflight) {
2450
2473
  const buildRequest = {
2451
2474
  name: campaignName,
@@ -2478,6 +2501,11 @@ function createBuildRequest(request, defaults, targetCount, campaignName, scopeB
2478
2501
  };
2479
2502
  const scoped = asRecord(scopeBuildArgs);
2480
2503
  buildRequest.gtmCampaignId = request.gtmCampaignId ?? stringValue(scoped.gtm_campaign_id ?? scoped.gtmCampaignId);
2504
+ buildRequest.learningHoldout = normalizeCampaignBuilderLearningHoldout(
2505
+ request.learningHoldout ?? scoped.learning_holdout ?? scoped.learningHoldout,
2506
+ buildRequest.gtmCampaignId,
2507
+ campaignName
2508
+ );
2481
2509
  if (typeof scoped.name === "string") buildRequest.name = scoped.name;
2482
2510
  if (typeof scoped.prompt === "string") buildRequest.prompt = scoped.prompt;
2483
2511
  if (typeof scoped.target_count === "number") buildRequest.targetCount = scoped.target_count;
@@ -2552,6 +2580,7 @@ function createBrainPreflight(request, buildRequest, routes, memory) {
2552
2580
  target_icp: targetIcp,
2553
2581
  provider_chain: providerChain,
2554
2582
  lead_source: providerChain[0] ?? "signaliz",
2583
+ learning_holdout: buildRequest.learningHoldout,
2555
2584
  tool_sequence: [
2556
2585
  { tool: "gtm_brain_learning_cycle_plan", purpose: "Plan the Brain learning loop for this campaign.", arguments: learningArgs },
2557
2586
  { tool: "gtm_brain_seed_defaults", purpose: "Seed campaign defaults from workspace memory and safe aggregate patterns.", arguments: defaultsArgs },
@@ -3219,6 +3248,16 @@ function buildCampaignArgs(request) {
3219
3248
  if (request.dedupKeys) args.dedup_keys = request.dedupKeys;
3220
3249
  if (request.brainPreflight) args.brain_preflight = request.brainPreflight;
3221
3250
  if (request.brainDefaults) args.brain_defaults = request.brainDefaults;
3251
+ if (request.learningHoldout) {
3252
+ args.learning_holdout = compact({
3253
+ enabled: request.learningHoldout.enabled,
3254
+ control_percentage: request.learningHoldout.controlPercentage,
3255
+ min_control_size: request.learningHoldout.minControlSize,
3256
+ experiment_key: request.learningHoldout.experimentKey,
3257
+ source_campaign_id: request.learningHoldout.sourceCampaignId,
3258
+ primary_metric: request.learningHoldout.primaryMetric
3259
+ });
3260
+ }
3222
3261
  if (request.campaignStrategyContext) args.campaign_strategy_context = request.campaignStrategyContext;
3223
3262
  if (request.deliveryRisk) args.delivery_risk = request.deliveryRisk;
3224
3263
  if (request.icp) {
@@ -3315,6 +3354,7 @@ function buildExecutionPrepareArgs(request, dryRun, confirmSpend) {
3315
3354
  delivery_risk: buildArgs2.delivery_risk,
3316
3355
  dedup_keys: buildArgs2.dedup_keys,
3317
3356
  policy: buildArgs2.policy,
3357
+ learning_holdout: buildArgs2.learning_holdout,
3318
3358
  signals: buildArgs2.signals,
3319
3359
  qualification: buildArgs2.qualification,
3320
3360
  copy: buildArgs2.copy,
@@ -3349,10 +3389,143 @@ function mapBuildResult(data, dryRun) {
3349
3389
  targetLimitReason: data.target_limit_reason,
3350
3390
  maxSupportedTargetCount: data.max_supported_target_count,
3351
3391
  brainContext: data.brain_context ?? {},
3392
+ learningHoldout: data.learning_holdout ?? {},
3352
3393
  providerRoute: mapProviderRoute2(data)
3353
3394
  };
3354
3395
  }
3396
+ function mapAgentCampaignPhaseHealth(value) {
3397
+ if (!Array.isArray(value)) return [];
3398
+ return value.map((phase) => {
3399
+ const record = asRecord(phase);
3400
+ return {
3401
+ phase: stringValue(record.phase) ?? "unknown",
3402
+ status: stringValue(record.status) ?? "unknown",
3403
+ recordsProcessed: numberOrNull2(record.records_processed ?? record.recordsProcessed),
3404
+ recordsSucceeded: numberOrNull2(record.records_succeeded ?? record.recordsSucceeded),
3405
+ recordsFailed: numberOrNull2(record.records_failed ?? record.recordsFailed),
3406
+ heartbeatAgeSeconds: numberOrNull2(record.heartbeat_age_seconds ?? record.heartbeatAgeSeconds ?? record.phase_age_seconds)
3407
+ };
3408
+ });
3409
+ }
3410
+ function deriveCampaignBuildTerminalState(input) {
3411
+ const status = input.status.toLowerCase();
3412
+ const heartbeatAgeSeconds = input.phaseAgeSeconds;
3413
+ const stale = input.staleRunningPhase === true;
3414
+ const safeWatchAction = input.triggerRunId ? `signaliz ops run-status ${input.triggerRunId} --watch` : "signaliz campaign-agent status <campaign_build_id>";
3415
+ if (CAMPAIGN_BUILDER_BLOCKED_TERMINAL_STATUSES.has(status)) {
3416
+ return {
3417
+ kind: "blocked",
3418
+ label: "Blocked terminal state",
3419
+ reviewable: true,
3420
+ terminal: true,
3421
+ stale: false,
3422
+ phase: input.phase,
3423
+ phaseStatus: input.phaseStatus,
3424
+ heartbeatAgeSeconds,
3425
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
3426
+ terminalReason: input.terminalReason,
3427
+ staleReason: null,
3428
+ safeNextAction: input.nextAction || "Review errors and rerun only after the blocker is resolved."
3429
+ };
3430
+ }
3431
+ if (status === "pending_approval") {
3432
+ return {
3433
+ kind: "pending_approval",
3434
+ label: "Pending delivery approval",
3435
+ reviewable: true,
3436
+ terminal: false,
3437
+ stale: false,
3438
+ phase: input.phase,
3439
+ phaseStatus: input.phaseStatus,
3440
+ heartbeatAgeSeconds,
3441
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
3442
+ terminalReason: input.terminalReason,
3443
+ staleReason: null,
3444
+ safeNextAction: input.approvalAction || input.nextAction || "Review rows and approve delivery only after the approval packet passes."
3445
+ };
3446
+ }
3447
+ if (CAMPAIGN_BUILDER_TERMINAL_STATUSES.has(status)) {
3448
+ return {
3449
+ kind: "terminal",
3450
+ label: "Terminal and reviewable",
3451
+ reviewable: true,
3452
+ terminal: true,
3453
+ stale: false,
3454
+ phase: input.phase,
3455
+ phaseStatus: input.phaseStatus,
3456
+ heartbeatAgeSeconds,
3457
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
3458
+ terminalReason: input.terminalReason,
3459
+ staleReason: null,
3460
+ safeNextAction: input.nextAction || "Review rows, artifacts, scorecard, and approval locks before delivery."
3461
+ };
3462
+ }
3463
+ if (status === "running" || status === "queued" || status === "processing") {
3464
+ if (stale) {
3465
+ return {
3466
+ kind: "running_stale",
3467
+ label: "Running but stale",
3468
+ reviewable: false,
3469
+ terminal: false,
3470
+ stale: true,
3471
+ phase: input.phase,
3472
+ phaseStatus: input.phaseStatus,
3473
+ heartbeatAgeSeconds,
3474
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
3475
+ terminalReason: null,
3476
+ staleReason: input.staleReason || "The active phase heartbeat is stale.",
3477
+ safeNextAction: input.nextAction || safeWatchAction
3478
+ };
3479
+ }
3480
+ return {
3481
+ kind: "running_healthy",
3482
+ label: "Running and healthy",
3483
+ reviewable: false,
3484
+ terminal: false,
3485
+ stale: false,
3486
+ phase: input.phase,
3487
+ phaseStatus: input.phaseStatus,
3488
+ heartbeatAgeSeconds,
3489
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
3490
+ terminalReason: null,
3491
+ staleReason: null,
3492
+ safeNextAction: input.nextAction || safeWatchAction
3493
+ };
3494
+ }
3495
+ return {
3496
+ kind: "unknown",
3497
+ label: "Unknown build state",
3498
+ reviewable: false,
3499
+ terminal: false,
3500
+ stale,
3501
+ phase: input.phase,
3502
+ phaseStatus: input.phaseStatus,
3503
+ heartbeatAgeSeconds,
3504
+ nextPollAfterSeconds: input.nextPollAfterSeconds,
3505
+ terminalReason: input.terminalReason,
3506
+ staleReason: input.staleReason,
3507
+ safeNextAction: input.nextAction || "Read campaign build status again before making a launch or delivery decision."
3508
+ };
3509
+ }
3355
3510
  function mapAgentCampaignBuildStatus(data) {
3511
+ const customerRowCounts = mapAgentCustomerRowCounts(data.customer_row_counts);
3512
+ const phaseHealth = mapAgentCampaignPhaseHealth(data.phases);
3513
+ const terminalReason = stringValue(data.terminal_reason) ?? customerRowCounts?.terminalReason ?? null;
3514
+ const staleReason = stringValue(data.stale_reason) ?? stringValue(asRecord(data.diagnostics).stale_reason) ?? null;
3515
+ const nextPollAfterSeconds = numberOrNull2(data.next_poll_after_seconds ?? data.nextPollAfterSeconds);
3516
+ const terminalState = deriveCampaignBuildTerminalState({
3517
+ status: stringValue(data.status) ?? "unknown",
3518
+ phase: stringValue(data.current_phase) ?? null,
3519
+ phaseStatus: stringValue(data.current_phase_status) ?? null,
3520
+ staleRunningPhase: data.stale_running_phase === true,
3521
+ phaseAgeSeconds: numberOrNull2(data.phase_age_seconds),
3522
+ nextPollAfterSeconds,
3523
+ terminalReason,
3524
+ staleReason,
3525
+ nextAction: formatAgentNextAction(data.next_action),
3526
+ approvalAction: formatAgentNextAction(data.approval_action),
3527
+ triggerRunId: stringValue(data.trigger_run_id) ?? null
3528
+ });
3356
3529
  return {
3357
3530
  campaignBuildId: stringValue(data.campaign_build_id) ?? "",
3358
3531
  campaignId: stringValue(data.campaign_id) ?? null,
@@ -3370,12 +3543,18 @@ function mapAgentCampaignBuildStatus(data) {
3370
3543
  errors: diagnosticMessages2(data.errors),
3371
3544
  artifactCount: numberOrUndefined2(data.artifact_count) ?? 0,
3372
3545
  artifactDownloads: Array.isArray(data.artifact_downloads) ? data.artifact_downloads.map(mapAgentCampaignArtifactDownload) : [],
3373
- customerRowCounts: mapAgentCustomerRowCounts(data.customer_row_counts),
3546
+ customerRowCounts,
3374
3547
  phaseAttemptTotals: nullableRecord(data.phase_attempt_totals) ?? void 0,
3375
3548
  providerRoute: mapProviderRoute2(data),
3376
3549
  triggerRunId: stringValue(data.trigger_run_id) ?? null,
3377
3550
  staleRunningPhase: data.stale_running_phase === true,
3378
3551
  phaseAgeSeconds: numberOrNull2(data.phase_age_seconds),
3552
+ nextPollAfterSeconds,
3553
+ terminalReason,
3554
+ staleReason,
3555
+ safeNextAction: terminalState.safeNextAction,
3556
+ terminalState,
3557
+ phaseHealth,
3379
3558
  diagnostics: nullableRecord(data.diagnostics) ?? {},
3380
3559
  nextAction: formatAgentNextAction(data.next_action),
3381
3560
  approvalAction: formatAgentNextAction(data.approval_action),
@@ -3463,67 +3642,276 @@ function mapAgentCampaignArtifact(data) {
3463
3642
  createdAt: stringValue(data.created_at) ?? ""
3464
3643
  };
3465
3644
  }
3645
+ function campaignReviewGate(id, label, status, detail) {
3646
+ return {
3647
+ id,
3648
+ label,
3649
+ status,
3650
+ passed: status !== "blocked",
3651
+ detail
3652
+ };
3653
+ }
3654
+ function northStarMetric(id, label, status, detail, value, target) {
3655
+ return {
3656
+ id,
3657
+ label,
3658
+ status,
3659
+ detail,
3660
+ ...value !== void 0 ? { value } : {},
3661
+ ...target !== void 0 ? { target } : {}
3662
+ };
3663
+ }
3664
+ function createCampaignBuilderNorthStarScorecard(status, rows, artifacts, terminalState) {
3665
+ if (!terminalState.reviewable) {
3666
+ const pendingMetric = northStarMetric(
3667
+ "terminal_state",
3668
+ "Terminal-state contract",
3669
+ terminalState.kind === "running_stale" ? "blocked" : "pending",
3670
+ 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.`,
3671
+ terminalState.kind,
3672
+ "terminal or pending_approval"
3673
+ );
3674
+ return {
3675
+ version: "campaign-builder-north-star.v1",
3676
+ status: pendingMetric.status,
3677
+ score: null,
3678
+ moatState: "scaffolding_ready",
3679
+ metrics: [pendingMetric],
3680
+ blockers: pendingMetric.status === "blocked" ? [pendingMetric.detail] : [],
3681
+ warnings: pendingMetric.status === "pending" ? [pendingMetric.detail] : [],
3682
+ nextAction: terminalState.safeNextAction
3683
+ };
3684
+ }
3685
+ const sampledRows = rows.rows.length;
3686
+ const rowData = rows.rows.map((row) => asRecord(row.data));
3687
+ const requestedTarget = status.customerRowCounts?.requestedTarget ?? status.recordsTotal ?? rows.count;
3688
+ const finalRows = status.customerRowCounts?.deliveredRows ?? status.customerRowCounts?.qaReadyRows ?? status.customerRowCounts?.copiedRows ?? status.customerRowCounts?.acceptedRows ?? rows.count;
3689
+ const rowsWithEmail = rows.rows.filter(campaignReviewRowHasEmail).length;
3690
+ const verifiedEmailRows = rowData.filter(campaignReviewDataHasVerifiedEmail).length;
3691
+ const uniqueEmails = new Set(rowData.map(campaignReviewDataEmail).filter(Boolean)).size;
3692
+ const uniqueDomains = new Set(rowData.map(campaignReviewDataDomain).filter(Boolean)).size;
3693
+ const copyReadyRows = rows.rows.filter(campaignReviewRowHasCopy).length;
3694
+ const copyQaIssuesByRow = rowData.map(campaignReviewDataCopyQaIssues).filter((issues) => issues.length > 0);
3695
+ const copyQaIssueCounts = countCampaignReviewIssues(copyQaIssuesByRow);
3696
+ const signalBackedRows = rowData.filter(campaignReviewDataHasSignalBasis).length;
3697
+ const noSupportedSignalRows = rowData.filter(campaignReviewDataHasNoSupportedSignal).length;
3698
+ const unsupportedSignalClaims = rowData.filter(campaignReviewDataHasUnsupportedSignalClaim).length;
3699
+ const approvalLockedRows = rowData.filter(campaignReviewDataHasApprovalLock).length;
3700
+ const holdoutRows = rowData.filter(campaignReviewDataHasHoldoutAttribution).length;
3701
+ const artifactCount = artifacts.length || status.artifactCount || 0;
3702
+ const deliveredRows = status.customerRowCounts?.deliveredRows ?? 0;
3703
+ const feedbackReady = campaignReviewStatusHasFeedbackReadiness(status);
3704
+ const feedbackRequiredForApproval = terminalState.kind === "pending_approval";
3705
+ const moat = campaignReviewMoatState(status, holdoutRows);
3706
+ const metrics = [
3707
+ northStarMetric(
3708
+ "terminal_state",
3709
+ "Terminal-state contract",
3710
+ terminalState.kind === "blocked" ? "blocked" : "pass",
3711
+ terminalState.label,
3712
+ terminalState.kind,
3713
+ "terminal or pending_approval"
3714
+ ),
3715
+ northStarMetric(
3716
+ "target_fill",
3717
+ "Target fill",
3718
+ requestedTarget && finalRows < requestedTarget ? "review" : "pass",
3719
+ requestedTarget ? `${finalRows || 0}/${requestedTarget} final or reviewable row(s) are accounted for.` : "No requested target was reported; using sampled rows for review.",
3720
+ finalRows || sampledRows,
3721
+ requestedTarget || sampledRows
3722
+ ),
3723
+ northStarMetric(
3724
+ "unique_emails",
3725
+ "Unique emails",
3726
+ sampledRows === 0 ? "blocked" : uniqueEmails === rowsWithEmail ? "pass" : "blocked",
3727
+ sampledRows === 0 ? "No sampled rows were available for email uniqueness review." : `${uniqueEmails}/${rowsWithEmail} sampled email(s) are unique.`,
3728
+ uniqueEmails,
3729
+ rowsWithEmail
3730
+ ),
3731
+ northStarMetric(
3732
+ "unique_domains",
3733
+ "Unique domains",
3734
+ sampledRows === 0 ? "blocked" : uniqueDomains === sampledRows ? "pass" : "review",
3735
+ sampledRows === 0 ? "No sampled rows were available for domain uniqueness review." : `${uniqueDomains}/${sampledRows} sampled company domain(s) are unique.`,
3736
+ uniqueDomains,
3737
+ sampledRows
3738
+ ),
3739
+ northStarMetric(
3740
+ "verified_email_rows",
3741
+ "Verified email rows",
3742
+ sampledRows === 0 ? "blocked" : verifiedEmailRows === sampledRows ? "pass" : rowsWithEmail === sampledRows ? "review" : "blocked",
3743
+ sampledRows === 0 ? "No sampled rows were available for email verification review." : `${verifiedEmailRows}/${sampledRows} sampled row(s) carry verified-email evidence.`,
3744
+ verifiedEmailRows,
3745
+ sampledRows
3746
+ ),
3747
+ northStarMetric(
3748
+ "copy_completeness",
3749
+ "Copy completeness",
3750
+ sampledRows === 0 ? "blocked" : copyReadyRows === sampledRows ? "pass" : "blocked",
3751
+ sampledRows === 0 ? "No sampled rows were available for copy review." : `${copyReadyRows}/${sampledRows} sampled row(s) include generated copy.`,
3752
+ copyReadyRows,
3753
+ sampledRows
3754
+ ),
3755
+ northStarMetric(
3756
+ "copy_quality",
3757
+ "Copy QA",
3758
+ copyQaIssuesByRow.length === 0 ? "pass" : "blocked",
3759
+ copyQaIssuesByRow.length === 0 ? "No sampled copy QA blockers were detected." : `${copyQaIssuesByRow.length}/${sampledRows} sampled row(s) have copy QA issue(s): ${formatCampaignReviewIssueCounts(copyQaIssueCounts)}.`,
3760
+ copyQaIssuesByRow.length,
3761
+ 0
3762
+ ),
3763
+ northStarMetric(
3764
+ "signal_grounding",
3765
+ "Signal grounding",
3766
+ unsupportedSignalClaims > 0 ? "blocked" : signalBackedRows > 0 ? "pass" : "review",
3767
+ 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.",
3768
+ signalBackedRows,
3769
+ sampledRows
3770
+ ),
3771
+ northStarMetric(
3772
+ "approval_locks",
3773
+ "Approval locks",
3774
+ approvalLockedRows > 0 || deliveredRows === 0 ? "pass" : "review",
3775
+ 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.",
3776
+ approvalLockedRows,
3777
+ sampledRows
3778
+ ),
3779
+ northStarMetric(
3780
+ "export_send_safety",
3781
+ "Export/send safety",
3782
+ deliveredRows > 0 && status.status !== "completed" ? "blocked" : "pass",
3783
+ deliveredRows > 0 ? `${deliveredRows} row(s) are delivered; status is ${status.status}.` : "No delivered rows are reported before approval.",
3784
+ deliveredRows,
3785
+ 0
3786
+ ),
3787
+ northStarMetric(
3788
+ "holdout_attribution",
3789
+ "Holdout attribution",
3790
+ holdoutRows > 0 ? "pass" : "review",
3791
+ holdoutRows > 0 ? `${holdoutRows}/${sampledRows} sampled row(s) preserve holdout attribution.` : "No holdout attribution was visible in the sampled rows.",
3792
+ holdoutRows,
3793
+ sampledRows
3794
+ ),
3795
+ northStarMetric(
3796
+ "feedback_readiness",
3797
+ "Feedback readiness",
3798
+ feedbackReady ? "pass" : feedbackRequiredForApproval ? "blocked" : "review",
3799
+ 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.",
3800
+ feedbackReady,
3801
+ true
3802
+ ),
3803
+ northStarMetric(
3804
+ "causal_moat",
3805
+ "Causal moat",
3806
+ moat.state === "proved" ? "pass" : "review",
3807
+ moat.detail,
3808
+ moat.state,
3809
+ "proved"
3810
+ ),
3811
+ northStarMetric(
3812
+ "artifact_readback",
3813
+ "Artifact readback",
3814
+ artifactCount > 0 ? "pass" : "review",
3815
+ artifactCount > 0 ? `${artifactCount} artifact(s) are available for review.` : "No review artifact is available yet.",
3816
+ artifactCount,
3817
+ 1
3818
+ )
3819
+ ];
3820
+ const gradedMetrics = metrics.filter((metric) => metric.status !== "pending");
3821
+ const score = gradedMetrics.length === 0 ? null : Math.round(gradedMetrics.filter((metric) => metric.status === "pass").length / gradedMetrics.length * 100);
3822
+ const blockers = metrics.filter((metric) => metric.status === "blocked").map((metric) => metric.detail);
3823
+ const warnings = metrics.filter((metric) => metric.status === "review").map((metric) => metric.detail);
3824
+ const scorecardStatus = blockers.length > 0 ? "blocked" : warnings.length > 0 ? "review" : "pass";
3825
+ return {
3826
+ version: "campaign-builder-north-star.v1",
3827
+ status: scorecardStatus,
3828
+ score,
3829
+ moatState: moat.state,
3830
+ metrics,
3831
+ blockers,
3832
+ warnings,
3833
+ nextAction: scorecardStatus === "blocked" ? "Fix blocked North Star gates before approval or delivery." : terminalState.safeNextAction
3834
+ };
3835
+ }
3466
3836
  function createCampaignBuilderBuildReview(status, rows, artifacts) {
3837
+ const terminalState = status.terminalState ?? deriveCampaignBuildTerminalState({
3838
+ status: status.status,
3839
+ phase: status.currentPhase,
3840
+ phaseStatus: status.currentPhaseStatus,
3841
+ staleRunningPhase: status.staleRunningPhase === true,
3842
+ phaseAgeSeconds: status.phaseAgeSeconds ?? null,
3843
+ nextPollAfterSeconds: status.nextPollAfterSeconds ?? null,
3844
+ terminalReason: status.terminalReason ?? status.customerRowCounts?.terminalReason ?? null,
3845
+ staleReason: status.staleReason ?? null,
3846
+ nextAction: status.nextAction,
3847
+ approvalAction: status.approvalAction,
3848
+ triggerRunId: status.triggerRunId ?? null
3849
+ });
3467
3850
  const sampledRows = rows.rows.length;
3468
3851
  const availableRows = rows.count || sampledRows;
3469
3852
  const qualifiedRows = rows.rows.filter((row) => row.qualified !== false).length;
3470
3853
  const rowsWithEmail = rows.rows.filter(campaignReviewRowHasEmail).length;
3471
3854
  const copyReadyRows = rows.rows.filter(campaignReviewRowHasCopy).length;
3472
3855
  const artifactCount = artifacts.length || status.artifactCount || 0;
3856
+ const northStarScorecard = createCampaignBuilderNorthStarScorecard(status, rows, artifacts, terminalState);
3473
3857
  const gates = [
3474
- {
3475
- id: "build_completed",
3476
- label: "Build completed",
3477
- passed: status.status === "completed",
3478
- detail: status.status === "completed" ? "The campaign build is completed." : `The campaign build is ${status.status}; review again after completion.`
3479
- },
3480
- {
3481
- id: "rows_available",
3482
- label: "Rows available",
3483
- passed: sampledRows > 0,
3484
- detail: sampledRows > 0 ? `${sampledRows} sampled row(s) are available for review.` : "No rows were returned for review."
3485
- },
3486
- {
3487
- id: "qualified_sample",
3488
- label: "Sample rows qualified",
3489
- passed: sampledRows > 0 && qualifiedRows === sampledRows,
3490
- detail: sampledRows > 0 ? `${qualifiedRows}/${sampledRows} sampled row(s) are marked qualified.` : "Qualification could not be checked without sampled rows."
3491
- },
3492
- {
3493
- id: "email_coverage",
3494
- label: "Email coverage",
3495
- passed: sampledRows > 0 && rowsWithEmail === sampledRows,
3496
- detail: sampledRows > 0 ? `${rowsWithEmail}/${sampledRows} sampled row(s) include an email.` : "Email coverage could not be checked without sampled rows."
3497
- },
3498
- {
3499
- id: "artifact_available",
3500
- label: "Artifact available",
3501
- passed: artifactCount > 0,
3502
- detail: artifactCount > 0 ? `${artifactCount} artifact(s) are available.` : "No delivery artifact is available yet."
3503
- },
3504
- {
3505
- id: "no_failed_rows",
3506
- label: "No failed rows",
3507
- passed: status.recordsFailed === 0,
3508
- detail: status.recordsFailed === 0 ? "The build reports zero failed rows." : `${status.recordsFailed} row(s) failed during the build.`
3509
- }
3858
+ campaignReviewGate(
3859
+ "build_reviewable",
3860
+ "Build reviewable",
3861
+ terminalState.kind === "running_healthy" ? "pending" : terminalState.kind === "running_stale" || terminalState.kind === "blocked" ? "blocked" : "pass",
3862
+ terminalState.label
3863
+ ),
3864
+ campaignReviewGate(
3865
+ "rows_available",
3866
+ "Rows available",
3867
+ sampledRows > 0 ? "pass" : terminalState.reviewable ? "blocked" : "pending",
3868
+ 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."
3869
+ ),
3870
+ campaignReviewGate(
3871
+ "qualified_sample",
3872
+ "Sample rows qualified",
3873
+ sampledRows === 0 ? terminalState.reviewable ? "blocked" : "pending" : qualifiedRows === sampledRows ? "pass" : "blocked",
3874
+ sampledRows > 0 ? `${qualifiedRows}/${sampledRows} sampled row(s) are marked qualified.` : "Qualification could not be checked without sampled rows."
3875
+ ),
3876
+ campaignReviewGate(
3877
+ "email_coverage",
3878
+ "Email coverage",
3879
+ sampledRows === 0 ? terminalState.reviewable ? "blocked" : "pending" : rowsWithEmail === sampledRows ? "pass" : "blocked",
3880
+ sampledRows > 0 ? `${rowsWithEmail}/${sampledRows} sampled row(s) include an email.` : "Email coverage could not be checked without sampled rows."
3881
+ ),
3882
+ campaignReviewGate(
3883
+ "artifact_available",
3884
+ "Artifact available",
3885
+ artifactCount > 0 ? "pass" : terminalState.reviewable ? "review" : "pending",
3886
+ artifactCount > 0 ? `${artifactCount} artifact(s) are available.` : "No delivery artifact is available yet."
3887
+ ),
3888
+ campaignReviewGate(
3889
+ "no_failed_rows",
3890
+ "No failed rows",
3891
+ status.recordsFailed === 0 ? "pass" : "blocked",
3892
+ status.recordsFailed === 0 ? "The build reports zero failed rows." : `${status.recordsFailed} row(s) failed during the build.`
3893
+ )
3510
3894
  ];
3511
- const blockedReasons = gates.filter((gate) => !gate.passed).map((gate) => gate.detail);
3895
+ const blockedReasons = gates.filter((gate) => gate.status === "blocked" || !gate.passed).map((gate) => gate.detail);
3512
3896
  const warnings = [
3513
3897
  rows.hasMore ? "This review is based on a row sample; page through remaining rows before final approval." : void 0,
3514
3898
  sampledRows > 0 && copyReadyRows < sampledRows ? `${copyReadyRows}/${sampledRows} sampled row(s) include generated copy.` : void 0,
3515
3899
  ...status.warnings
3516
3900
  ].filter((item) => Boolean(item));
3517
- const readyForDeliveryApproval = blockedReasons.length === 0;
3901
+ const readyForDeliveryApproval = terminalState.reviewable && blockedReasons.length === 0 && northStarScorecard.status !== "blocked";
3518
3902
  return {
3519
3903
  campaignBuildId: status.campaignBuildId || rows.campaignBuildId,
3520
3904
  status,
3521
3905
  rows,
3522
3906
  artifacts,
3523
3907
  gates,
3908
+ northStarScorecard,
3524
3909
  summary: {
3525
3910
  status: status.status,
3526
3911
  phase: status.currentPhase,
3912
+ terminalState: terminalState.kind,
3913
+ scorecardStatus: northStarScorecard.status,
3914
+ scorecardScore: northStarScorecard.score,
3527
3915
  sampledRows,
3528
3916
  availableRows,
3529
3917
  qualifiedRows,
@@ -3577,6 +3965,184 @@ function campaignReviewRowHasCopy(row) {
3577
3965
  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)
3578
3966
  );
3579
3967
  }
3968
+ function campaignReviewDataEmail(data) {
3969
+ return stringValue(data.email) || stringValue(data.work_email) || stringValue(data.workEmail) || stringValue(asRecord(data.contact).email);
3970
+ }
3971
+ function campaignReviewDataDomain(data) {
3972
+ const domain = stringValue(data.company_domain) || stringValue(data.companyDomain) || stringValue(asRecord(data.company).domain);
3973
+ return domain?.toLowerCase();
3974
+ }
3975
+ function campaignReviewDataHasVerifiedEmail(data) {
3976
+ const status = String(data.email_status ?? data.emailStatus ?? asRecord(data.contact).email_status ?? "").toLowerCase();
3977
+ return data.email_verified === true || data.emailVerified === true || asRecord(data.contact).email_verified === true || ["valid", "validated", "verified"].includes(status);
3978
+ }
3979
+ function campaignReviewDataCopyParts(data) {
3980
+ const copy = asRecord(data.copy);
3981
+ const rawCopy = asRecord(data.raw_copy ?? data.rawCopy);
3982
+ return [
3983
+ stringValue(data.subject),
3984
+ stringValue(data.subject_line),
3985
+ stringValue(data.opener),
3986
+ stringValue(data.body),
3987
+ stringValue(data.cta),
3988
+ stringValue(copy.subject),
3989
+ stringValue(copy.opener),
3990
+ stringValue(copy.body),
3991
+ stringValue(copy.cta),
3992
+ stringValue(rawCopy.subject),
3993
+ stringValue(rawCopy.opener),
3994
+ stringValue(rawCopy.body),
3995
+ stringValue(rawCopy.cta)
3996
+ ].filter((part) => Boolean(part));
3997
+ }
3998
+ function campaignReviewDataHasBadGreeting(data) {
3999
+ return campaignReviewDataCopyParts(data).some(
4000
+ (part) => /\bhi\s*(?:,|\{\{|\[)|\bhello\s*(?:,|\{\{|\[)/i.test(part) || /\b(first_name|firstname|name)\b/i.test(part)
4001
+ );
4002
+ }
4003
+ function campaignReviewDataHasBannedPhrase(data) {
4004
+ const text = campaignReviewDataCopyParts(data).join(" ").toLowerCase();
4005
+ return NORTH_STAR_BANNED_COPY_PHRASES.some((phrase) => text.includes(phrase));
4006
+ }
4007
+ function campaignReviewDataSubject(data) {
4008
+ const copy = asRecord(data.copy);
4009
+ const rawCopy = asRecord(data.raw_copy ?? data.rawCopy);
4010
+ return stringValue(data.subject) || stringValue(data.subject_line) || stringValue(data.subjectLine) || stringValue(copy.subject) || stringValue(rawCopy.subject);
4011
+ }
4012
+ function campaignReviewDataCta(data) {
4013
+ const copy = asRecord(data.copy);
4014
+ const rawCopy = asRecord(data.raw_copy ?? data.rawCopy);
4015
+ return stringValue(data.cta) || stringValue(copy.cta) || stringValue(rawCopy.cta);
4016
+ }
4017
+ function countCampaignReviewIssues(issueRows) {
4018
+ return issueRows.flat().reduce((acc, issue) => {
4019
+ acc[issue] = (acc[issue] || 0) + 1;
4020
+ return acc;
4021
+ }, {});
4022
+ }
4023
+ function formatCampaignReviewIssueCounts(counts) {
4024
+ const entries = Object.entries(counts);
4025
+ return entries.length > 0 ? entries.map(([issue, count]) => `${issue}=${count}`).join(", ") : "none";
4026
+ }
4027
+ function campaignReviewDataCopyQaIssues(data) {
4028
+ const issues = /* @__PURE__ */ new Set();
4029
+ const copyParts = campaignReviewDataCopyParts(data);
4030
+ if (copyParts.length === 0) return [];
4031
+ if (campaignReviewDataHasBadGreeting(data)) issues.add("bad_greeting");
4032
+ if (campaignReviewDataHasBannedPhrase(data)) issues.add("banned_or_vague_phrase");
4033
+ if (campaignReviewDataHasUnsupportedSignalClaim(data)) issues.add("unsupported_signal_claim");
4034
+ if (campaignReviewDataPersonalizationBasis(data).length === 0) issues.add("missing_personalization_basis");
4035
+ if (!campaignReviewDataCta(data)) issues.add("missing_cta");
4036
+ const subject = campaignReviewDataSubject(data);
4037
+ if (subject && subject.length > 70) issues.add("overlong_subject");
4038
+ return [...issues];
4039
+ }
4040
+ function campaignReviewDataPersonalizationBasis(data) {
4041
+ const rawCopy = asRecord(data.raw_copy ?? data.rawCopy);
4042
+ const copy = asRecord(data.copy);
4043
+ const metadata = asRecord(data.metadata);
4044
+ const signalEvidence = asRecord(data.signal_evidence ?? metadata.signal_evidence ?? metadata.signalEvidence);
4045
+ return [
4046
+ ...arrayOfStrings(data.personalization_basis) ?? [],
4047
+ ...arrayOfStrings(data.personalizationBasis) ?? [],
4048
+ ...arrayOfStrings(copy.personalization_basis) ?? [],
4049
+ ...arrayOfStrings(rawCopy.personalization_basis) ?? [],
4050
+ ...arrayOfStrings(metadata.personalization_basis) ?? [],
4051
+ ...arrayOfStrings(metadata.personalizationBasis) ?? [],
4052
+ ...arrayOfStrings(signalEvidence.personalization_basis) ?? []
4053
+ ];
4054
+ }
4055
+ function campaignReviewDataHasNoSupportedSignal(data) {
4056
+ const metadata = asRecord(data.metadata);
4057
+ const copy = asRecord(data.copy);
4058
+ const state = [
4059
+ data.copy_signal_state,
4060
+ data.signal_basis,
4061
+ data.signal_type,
4062
+ metadata.copy_signal_state,
4063
+ metadata.signal_basis,
4064
+ metadata.signal_type,
4065
+ copy.copy_signal_state
4066
+ ].map((value) => stringValue(value)?.toLowerCase()).filter(Boolean);
4067
+ return data.no_supported_signal === true || metadata.no_supported_signal === true || state.includes("no_supported_signal");
4068
+ }
4069
+ function campaignReviewDataHasSignalBasis(data) {
4070
+ if (campaignReviewDataHasNoSupportedSignal(data)) return false;
4071
+ const metadata = asRecord(data.metadata);
4072
+ const signal = asRecord(data.signal);
4073
+ const signalEvidence = asRecord(data.signal_evidence ?? metadata.signal_evidence ?? metadata.signalEvidence);
4074
+ const basis = campaignReviewDataPersonalizationBasis(data);
4075
+ return Boolean(
4076
+ 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))
4077
+ );
4078
+ }
4079
+ function campaignReviewDataHasUnsupportedSignalClaim(data) {
4080
+ const copyText = campaignReviewDataCopyParts(data).join(" ").toLowerCase();
4081
+ const claimsSignal = /\b(hiring|funding|raised|launched|expanding|appointed|announced|recent news|new role|job posting)\b/.test(copyText);
4082
+ return claimsSignal && !campaignReviewDataHasSignalBasis(data);
4083
+ }
4084
+ function campaignReviewDataHasApprovalLock(data) {
4085
+ const metadata = asRecord(data.fit_metadata ?? data.metadata);
4086
+ const approval = String(data.approval_status ?? data.delivery_approval_status ?? metadata.approval_status ?? "").toLowerCase();
4087
+ const exportStatus = String(data.export_status ?? data.exportStatus ?? "").toLowerCase();
4088
+ 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);
4089
+ }
4090
+ function campaignReviewDataHasHoldoutAttribution(data) {
4091
+ const fitMetadata = asRecord(data.fit_metadata ?? data.fitMetadata);
4092
+ const metadata = asRecord(data.metadata);
4093
+ const holdout = asRecord(
4094
+ fitMetadata.learning_holdout ?? fitMetadata.learningHoldout ?? metadata.learning_holdout ?? metadata.learningHoldout ?? data.learning_holdout ?? data.learningHoldout
4095
+ );
4096
+ const experimentKey = stringValue(holdout.experiment_key ?? holdout.experimentKey ?? data.experiment_key ?? data.experimentKey);
4097
+ const cohort = stringValue(holdout.cohort_role ?? holdout.cohortRole ?? data.cohort_role ?? data.learning_cohort ?? data.learningCohort);
4098
+ const brainApplied = holdout.brain_applied ?? holdout.brainApplied ?? data.brain_applied ?? data.signaliz_brain_applied;
4099
+ return Boolean(experimentKey && cohort && brainApplied !== void 0);
4100
+ }
4101
+ function campaignReviewStatusHasFeedbackReadiness(status) {
4102
+ const diagnostics = asRecord(status.diagnostics);
4103
+ const feedbackReadiness = asRecord(diagnostics.feedback_readiness ?? diagnostics.feedbackReadiness);
4104
+ const feedbackSubstrate = asRecord(diagnostics.feedback_substrate ?? diagnostics.feedbackSubstrate);
4105
+ const evidenceCounts = asRecord(diagnostics.evidence_counts ?? diagnostics.evidenceCounts);
4106
+ return feedbackReadiness.ready === true || feedbackReadiness.webhook_configured === true || feedbackSubstrate.stage === "ready" || Number(evidenceCounts.feedback_events ?? 0) > 0;
4107
+ }
4108
+ function campaignReviewMoatState(status, holdoutRows) {
4109
+ const diagnostics = asRecord(status.diagnostics);
4110
+ const evidenceCounts = asRecord(diagnostics.evidence_counts ?? diagnostics.evidenceCounts);
4111
+ const embeddedHoldout = asRecord(diagnostics.embedded_holdout ?? diagnostics.embeddedHoldout);
4112
+ const holdoutLift = asRecord(diagnostics.holdout_lift ?? diagnostics.holdoutLift ?? diagnostics.holdout_measurement ?? diagnostics.holdoutMeasurement);
4113
+ const controlSample = Math.max(
4114
+ Number(evidenceCounts.holdout_control_sample_size ?? 0),
4115
+ Number(evidenceCounts.embedded_holdout_control_sample ?? 0),
4116
+ Number(embeddedHoldout.control_sample_size ?? 0)
4117
+ );
4118
+ const learnedSample = Math.max(
4119
+ Number(evidenceCounts.holdout_learned_sample_size ?? 0),
4120
+ Number(evidenceCounts.embedded_holdout_learned_sample ?? 0),
4121
+ Number(embeddedHoldout.learned_sample_size ?? 0)
4122
+ );
4123
+ const feedbackEvents = Math.max(
4124
+ Number(evidenceCounts.feedback_events ?? 0),
4125
+ Number(evidenceCounts.campaign_build_feedback_events ?? 0),
4126
+ Number(evidenceCounts.delivery_holdout_sent_outcomes ?? 0)
4127
+ );
4128
+ const proved = diagnostics.closed_loop_proof_ready === true || holdoutLift.honest_measurement === true && holdoutLift.positive_lift === true;
4129
+ if (proved) {
4130
+ return {
4131
+ state: "proved",
4132
+ detail: "Real attributed feedback shows positive lift for explicit control and learned cohorts."
4133
+ };
4134
+ }
4135
+ if (controlSample > 0 && learnedSample > 0 && feedbackEvents > 0) {
4136
+ return {
4137
+ state: "moved_needs_lift_volume",
4138
+ detail: `Control and learned cohorts have attributed feedback (${controlSample} control, ${learnedSample} learned, ${feedbackEvents} feedback event(s)); keep collecting lift volume before claiming proof.`
4139
+ };
4140
+ }
4141
+ return {
4142
+ state: "scaffolding_ready",
4143
+ 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."
4144
+ };
4145
+ }
3580
4146
  function createCampaignBuilderReadinessLane(route, plan) {
3581
4147
  const builtIn = campaignBuilderBuiltInForRoute(route);
3582
4148
  const customRoute = route.provider !== "signaliz" && route.provider !== "csv" && builtIn === void 0;
@@ -3877,7 +4443,7 @@ function hashString(input) {
3877
4443
  function errorMessage(error) {
3878
4444
  return error instanceof Error ? error.message : String(error);
3879
4445
  }
3880
- var DEFAULT_SIGNALIZ_CAMPAIGN_BUILDER_DEFAULTS, DEFAULT_CAMPAIGN_BUILDER_BUILT_INS, CAMPAIGN_BUILDER_BUILT_IN_ROUTE_IDS, DEFAULT_CAMPAIGN_BUILDER_APPROVAL_TYPES, CAMPAIGN_BUILDER_OPERATING_PLAYBOOKS, CAMPAIGN_BUILDER_STRATEGY_TEMPLATES, CampaignBuilderAgent;
4446
+ var DEFAULT_SIGNALIZ_CAMPAIGN_BUILDER_DEFAULTS, DEFAULT_CAMPAIGN_BUILDER_BUILT_INS, CAMPAIGN_BUILDER_BUILT_IN_ROUTE_IDS, DEFAULT_CAMPAIGN_BUILDER_APPROVAL_TYPES, CAMPAIGN_BUILDER_OPERATING_PLAYBOOKS, CAMPAIGN_BUILDER_STRATEGY_TEMPLATES, CampaignBuilderAgent, CAMPAIGN_BUILDER_TERMINAL_STATUSES, CAMPAIGN_BUILDER_BLOCKED_TERMINAL_STATUSES, NORTH_STAR_BANNED_COPY_PHRASES;
3881
4447
  var init_campaign_builder_agent = __esm({
3882
4448
  "src/resources/campaign-builder-agent.ts"() {
3883
4449
  "use strict";
@@ -4459,6 +5025,7 @@ var init_campaign_builder_agent = __esm({
4459
5025
  },
4460
5026
  brain_defaults: plan.buildRequest.brainDefaults,
4461
5027
  brain_preflight: plan.buildRequest.brainPreflight,
5028
+ learning_holdout: plan.buildRequest.learningHoldout,
4462
5029
  campaign_strategy_context: plan.buildRequest.campaignStrategyContext,
4463
5030
  delivery_risk_preflight: plan.buildRequest.deliveryRisk,
4464
5031
  delivery_risk: plan.buildRequest.deliveryRisk
@@ -4472,7 +5039,8 @@ var init_campaign_builder_agent = __esm({
4472
5039
  estimated_credits: plan.estimatedCredits
4473
5040
  },
4474
5041
  brain_delivery_risk_preflight: plan.buildRequest.deliveryRisk,
4475
- delivery_risk: plan.buildRequest.deliveryRisk
5042
+ delivery_risk: plan.buildRequest.deliveryRisk,
5043
+ learning_holdout: plan.buildRequest.learningHoldout
4476
5044
  },
4477
5045
  approval_required: plan.approvals.some((approval) => approval.blocking),
4478
5046
  actor_type: "agent",
@@ -4788,6 +5356,17 @@ var init_campaign_builder_agent = __esm({
4788
5356
  return this.client.mcp("tools/call", { name: tool, arguments: compact(args) });
4789
5357
  }
4790
5358
  };
5359
+ CAMPAIGN_BUILDER_TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "canceled", "cancelled"]);
5360
+ CAMPAIGN_BUILDER_BLOCKED_TERMINAL_STATUSES = /* @__PURE__ */ new Set(["failed", "canceled", "cancelled"]);
5361
+ NORTH_STAR_BANNED_COPY_PHRASES = [
5362
+ "i have been following",
5363
+ "i saw that",
5364
+ "would you be opposed",
5365
+ "unlock new opportunities",
5366
+ "significant success",
5367
+ "we understand",
5368
+ "hope this finds you well"
5369
+ ];
4791
5370
  }
4792
5371
  });
4793
5372
 
@@ -7858,6 +8437,16 @@ var init_gtm_kernel = __esm({
7858
8437
  min_privacy_k: input.minPrivacyK
7859
8438
  });
7860
8439
  }
8440
+ /** Queue a dry-run-first bridge from imported historical feedback to attribution-only Campaign Build surrogates. */
8441
+ async runHistoricalCampaignBuildBridge(input = {}) {
8442
+ return this.callMcp("gtm_historical_campaign_build_bridge_run", {
8443
+ dry_run: input.dryRun,
8444
+ write_approved: input.writeApproved,
8445
+ max_events: input.maxEvents,
8446
+ max_campaigns: input.maxCampaigns,
8447
+ min_feedback_events: input.minFeedbackEvents
8448
+ });
8449
+ }
7861
8450
  /** Prepare a provider-agnostic feedback webhook URL for Smartlead, HeyReach, Airbyte, or custom sender events. */
7862
8451
  async prepareFeedbackWebhook(input) {
7863
8452
  return this.callMcp("gtm_feedback_webhook_prepare", {
@@ -9954,6 +10543,13 @@ function printCampaignAgentBuildStatus(status) {
9954
10543
  if (status.campaignId) console.log(`Campaign ID: ${status.campaignId}`);
9955
10544
  console.log(`Status: ${status.status}`);
9956
10545
  console.log(`Phase: ${status.currentPhase || "N/A"}`);
10546
+ const terminalState = asRecord4(status.terminalState);
10547
+ if (terminalState.kind) {
10548
+ console.log(`Terminal state: ${terminalState.kind} (${terminalState.label || "no label"})`);
10549
+ if (terminalState.heartbeatAgeSeconds !== void 0 && terminalState.heartbeatAgeSeconds !== null) {
10550
+ console.log(`Heartbeat age: ${terminalState.heartbeatAgeSeconds}s`);
10551
+ }
10552
+ }
9957
10553
  console.log(`Rows: ${status.recordsSucceeded}/${status.recordsTotal} succeeded, ${status.recordsFailed} failed`);
9958
10554
  printCampaignCustomerRowCounts(status);
9959
10555
  console.log(`Artifacts: ${status.artifactCount}`);
@@ -9975,6 +10571,8 @@ function printCampaignAgentBuildStatus(status) {
9975
10571
  console.log(`Next: ${status.nextAction}`);
9976
10572
  } else if (status.status === "pending_approval") {
9977
10573
  console.log(`Next: signaliz campaign-agent approve ${status.campaignBuildId} --destination-type webhook`);
10574
+ } else if (terminalState.safeNextAction) {
10575
+ console.log(`Next: ${terminalState.safeNextAction}`);
9978
10576
  }
9979
10577
  }
9980
10578
  function printCampaignCustomerRowCounts(status) {
@@ -9998,17 +10596,20 @@ function printCampaignAgentBuildReview(review) {
9998
10596
  console.log(`Campaign: ${review.status?.name || review.campaignBuildId}`);
9999
10597
  console.log(`Status: ${summary.status || review.status?.status}`);
10000
10598
  console.log(`Phase: ${summary.phase || review.status?.currentPhase || "N/A"}`);
10599
+ console.log(`Terminal state: ${summary.terminalState || review.status?.terminalState?.kind || "unknown"}`);
10001
10600
  console.log(`Rows: ${summary.sampledRows || 0} sampled, ${summary.availableRows || 0} available`);
10002
10601
  console.log(`Qualified: ${summary.qualifiedRows || 0}`);
10003
10602
  console.log(`Emails: ${summary.rowsWithEmail || 0}`);
10004
10603
  console.log(`Copy ready: ${summary.copyReadyRows || 0}`);
10005
10604
  console.log(`Artifacts: ${summary.artifactCount || 0}`);
10605
+ printCampaignAgentNorthStarScorecard(review.northStarScorecard);
10006
10606
  if (review.campaignBuildId) printCampaignArtifactDownload(review.status, review.campaignBuildId);
10007
10607
  console.log(`Delivery approval ready: ${summary.readyForDeliveryApproval === true ? "yes" : "no"}`);
10008
10608
  if (Array.isArray(review.gates) && review.gates.length > 0) {
10009
10609
  console.log("\nGates:");
10010
10610
  for (const gate of review.gates) {
10011
- console.log(`- ${gate.passed ? "PASS" : "BLOCKED"} ${gate.label}: ${gate.detail}`);
10611
+ const status = String(gate.status || (gate.passed ? "pass" : "blocked")).toUpperCase();
10612
+ console.log(`- ${status} ${gate.label}: ${gate.detail}`);
10012
10613
  }
10013
10614
  }
10014
10615
  const warnings = Array.isArray(summary.warnings) ? summary.warnings : [];
@@ -10022,6 +10623,21 @@ function printCampaignAgentBuildReview(review) {
10022
10623
  for (const action of nextActions) console.log(`- ${action}`);
10023
10624
  }
10024
10625
  }
10626
+ function printCampaignAgentNorthStarScorecard(scorecard) {
10627
+ const card = asRecord4(scorecard);
10628
+ if (!card.version) return;
10629
+ const score = card.score === null || card.score === void 0 ? "pending" : `${card.score}%`;
10630
+ console.log(`North Star: ${String(card.status || "unknown")} (${score})`);
10631
+ if (card.moatState) console.log(`Moat state: ${card.moatState}`);
10632
+ const metrics = Array.isArray(card.metrics) ? card.metrics : [];
10633
+ if (metrics.length > 0) {
10634
+ console.log("\nNorth Star gates:");
10635
+ for (const metric of metrics.slice(0, 12)) {
10636
+ const item = asRecord4(metric);
10637
+ console.log(`- ${String(item.status || "review").toUpperCase()} ${item.label || item.id}: ${item.detail || ""}`);
10638
+ }
10639
+ }
10640
+ }
10025
10641
  function formatCampaignAgentRows(rows) {
10026
10642
  return rows.map((row) => {
10027
10643
  const data = asRecord4(row.data);