@nextcommerce/campaigns-os 1.55.0 → 1.57.0
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/CHANGELOG.md +64 -0
- package/CONTEXT.md +11 -0
- package/campaign-spec/README.md +15 -1
- package/campaign-spec/dist/checkout-flow.d.ts +90 -0
- package/campaign-spec/dist/checkout-flow.js +229 -0
- package/campaign-spec/dist/index.d.ts +1 -0
- package/campaign-spec/dist/index.js +6 -1
- package/campaign-spec/dist/normalize.d.ts +3 -2
- package/campaign-spec/dist/normalize.js +7 -5
- package/campaign-spec/dist/routing.d.ts +6 -3
- package/campaign-spec/dist/routing.js +6 -3
- package/campaign-spec/dist/rules/checkout-forward-target.d.ts +17 -0
- package/campaign-spec/dist/rules/checkout-forward-target.js +48 -0
- package/campaign-spec/dist/rules/checkout-step-reaches-checkout.d.ts +12 -0
- package/campaign-spec/dist/rules/checkout-step-reaches-checkout.js +37 -0
- package/campaign-spec/dist/rules/exit-intent-validation.js +2 -1
- package/campaign-spec/dist/rules/index.d.ts +4 -0
- package/campaign-spec/dist/rules/index.js +12 -0
- package/campaign-spec/dist/rules/one-checkout-per-path.d.ts +25 -0
- package/campaign-spec/dist/rules/one-checkout-per-path.js +84 -0
- package/campaign-spec/dist/rules/pre-payment-forward-target.d.ts +12 -0
- package/campaign-spec/dist/rules/pre-payment-forward-target.js +42 -0
- package/campaign-spec/dist/rules/promo-code-input-validation.js +2 -1
- package/campaign-spec/dist/rules/schema-version.d.ts +9 -4
- package/campaign-spec/dist/rules/schema-version.js +10 -5
- package/campaign-spec/dist/types.d.ts +20 -3
- package/campaign-spec/dist/types.js +3 -2
- package/compatibility.json +2 -2
- package/contracts/fixtures/runtime-recipe/accept/current.json +6 -1
- package/contracts/fixtures/runtime-recipe/accept/minimal.json +6 -1
- package/contracts/fixtures/runtime-recipe/dist-states.json +1 -1
- package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +6 -1
- package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +6 -1
- package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +6 -1
- package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +6 -1
- package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +6 -1
- package/contracts/fixtures/runtime-recipe/reject/missing-required-field.json +1 -1
- package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +6 -1
- package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +6 -1
- package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +6 -1
- package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +5 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +6 -1
- package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +6 -1
- package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +6 -1
- package/contracts/release-ledger.json +705 -0
- package/contracts/runtime-recipe.campaigns-os-node-v1.json +6 -1
- package/contracts/supported-surface.json +8 -4
- package/docs/build-packet.md +52 -8
- package/docs/local-setup.md +1 -1
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/qa-and-test-orders.md +78 -18
- package/docs/runtime-readiness.md +8 -3
- package/docs/skills-revision.md +10 -10
- package/docs/supported-surface.md +1 -1
- package/docs/versioning.md +4 -1
- package/package.json +2 -1
- package/schemas/campaign-runtime-build-context.v0.schema.json +3 -1
- package/schemas/campaign-runtime-build-packet.v0.schema.json +3 -1
- package/schemas/campaign-spec.v5.schema.json +586 -0
- package/skills/campaign-lifecycle-orientation/SKILL.md +10 -8
- package/skills/campaign-readback-classification/SKILL.md +3 -3
- package/skills/campaign-run-evidence/SKILL.md +3 -3
- package/skills/contribution-intake/SKILL.md +3 -3
- package/skills/next-campaigns-build/SKILL.md +6 -4
- package/skills/next-campaigns-os/SKILL.md +3 -3
- package/skills/next-campaigns-os-setup/SKILL.md +3 -3
- package/skills/next-campaigns-polish/SKILL.md +21 -6
- package/skills/next-campaigns-qa/SKILL.md +4 -3
- package/skills.json +10 -10
- package/src/adapter-decision-contract.mjs +1 -1
- package/src/build-brief.mjs +14 -5
- package/src/cli-helpers.mjs +129 -0
- package/src/cli.mjs +121 -53
- package/src/commercial-journey.mjs +25 -2
- package/src/design-source-package.mjs +1 -1
- package/src/design-source-publication.mjs +7 -7
- package/src/doctor/checks.mjs +196 -13
- package/src/doctor/inspect.mjs +4 -2
- package/src/doctor/next-step.mjs +31 -11
- package/src/doctor-sidecar.mjs +5 -3
- package/src/gate-actions.mjs +21 -1
- package/src/input-refresh.mjs +6 -6
- package/src/local-preview-policy.mjs +110 -12
- package/src/polish-node.mjs +3 -3
- package/src/qa-browser.mjs +226 -34
- package/src/qa-cart-entry.mjs +78 -14
- package/src/qa-node.mjs +35 -33
- package/src/qa-test-order-topology.mjs +118 -1
- package/src/qa-verdict-discovery.mjs +21 -4
- package/src/runtime-state-ignore.mjs +7 -5
- package/src/source-html-intake.mjs +46 -4
- package/src/stage-ledger.mjs +4 -2
- package/src/stage-record.mjs +3 -3
package/src/cli.mjs
CHANGED
|
@@ -21,9 +21,10 @@ import { homedir } from "node:os";
|
|
|
21
21
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
22
22
|
import { shellToken } from "./shell-token.mjs";
|
|
23
23
|
import { declaredOrderBumps, declaredSelectorTiers } from "./commercial-journey.mjs";
|
|
24
|
+
import { checkoutPathFrom, entryPages, isCheckoutFlowPage, paymentPageForFunnel, upgradeCampaignSpec } from "../campaign-spec/dist/index.js";
|
|
24
25
|
import { diagnosticExport, diagnosticTextLines } from "./diagnostic.mjs";
|
|
25
26
|
import { observeProgress, PROGRESS_OBSERVATION } from "./progress-node.mjs";
|
|
26
|
-
import { HIDDEN_EAGER_MEDIA_ACTIONS, requiredActionText, substitutePacket } from "./gate-actions.mjs";
|
|
27
|
+
import { HIDDEN_EAGER_MEDIA_ACTIONS, qaRunCommand, requiredActionText, substitutePacket } from "./gate-actions.mjs";
|
|
27
28
|
import { orderPathDepthDriftText, orderPathDepthReconcileAction, orderPathDepthsDisagree, parseOrderPathDepthFlag } from "./proof-policy.mjs";
|
|
28
29
|
import { specMaterialHash, specHashesMatch } from "./spec-identity.mjs";
|
|
29
30
|
// The same predicate stage-ledger.mjs judges a mutator's result with, imported
|
|
@@ -252,6 +253,7 @@ import {
|
|
|
252
253
|
readStoreProfile,
|
|
253
254
|
} from "./spec-derive-store.mjs";
|
|
254
255
|
import { ROOT, cmd, asInvocation } from "./install-invocation.mjs";
|
|
256
|
+
import { hostedTemplateNeedsPreviewUrl, hostedTemplateNeedsUsablePreview } from "./local-preview-policy.mjs";
|
|
255
257
|
import {
|
|
256
258
|
requireArg,
|
|
257
259
|
isObject,
|
|
@@ -263,6 +265,7 @@ import {
|
|
|
263
265
|
sha256File,
|
|
264
266
|
relFromDir,
|
|
265
267
|
isLocalAbsolutePath,
|
|
268
|
+
portableArtifactPaths,
|
|
266
269
|
resolveFromFile,
|
|
267
270
|
extractFrontmatterValue,
|
|
268
271
|
addIssue,
|
|
@@ -302,6 +305,7 @@ const HELP = `Campaigns OS toolkit
|
|
|
302
305
|
|
|
303
306
|
Usage:
|
|
304
307
|
campaigns-os help
|
|
308
|
+
campaigns-os --version # print the installed package version; -v is equivalent
|
|
305
309
|
campaigns-os demo --target <new-directory> # offline inert sample; open landing/index.html; no campaign evidence
|
|
306
310
|
campaigns-os start (--spec <json> | --map-id <id>) --source <html-dir> --target <page-kit-dir> --template-family <family>
|
|
307
311
|
[--brief <yaml|json>] [--proxy-base <url>] [--cached-spec] [--theme-policy <inspect_only|auto|off>]
|
|
@@ -476,6 +480,12 @@ function closestCommand(input) {
|
|
|
476
480
|
// (src/qc-test-factories.mjs). The bin entry point never passes it, and no
|
|
477
481
|
// flag, environment variable or file can supply one.
|
|
478
482
|
export async function main(argv, { authentication, qcStandIns } = {}) {
|
|
483
|
+
// Global version flags bypass invocation policy, including session reads and
|
|
484
|
+
// lifecycle capture, just as help bypasses campaign effects.
|
|
485
|
+
if (argv[0] === "--version" || argv[0] === "-v") {
|
|
486
|
+
console.log(packageVersion());
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
479
489
|
return runInvocation(parseArgs(argv), {
|
|
480
490
|
dispatch: (command, args, context) => dispatch(command, args, { ...context, argv, authentication, qcStandIns }),
|
|
481
491
|
closeOutStaleRunSessions,
|
|
@@ -729,7 +739,7 @@ export function recordQaStageOutcome(args, result) {
|
|
|
729
739
|
disposition: verdict.disposition,
|
|
730
740
|
timestamp: verdict.completed_at,
|
|
731
741
|
command: `campaigns-os ${QA_RUN_PRODUCER}`,
|
|
732
|
-
outputs: [result.local_path, result.qa_sidecar?.path].filter(isNonEmptyString),
|
|
742
|
+
outputs: portableArtifactPaths([result.local_path, result.qa_sidecar?.path].filter(isNonEmptyString), workspace.targetRepo),
|
|
733
743
|
blockers: verdict.disposition === "blocked" ? failed : [],
|
|
734
744
|
warnings: verdict.disposition === "ready_with_exceptions"
|
|
735
745
|
? ["QA passed with explicitly attributed exceptions. Report them to the operator; do not clear or waive them, or change markup just to make them pass."]
|
|
@@ -4217,11 +4227,11 @@ const RUN_RECORD_QA_DIGEST_LIMIT = 8;
|
|
|
4217
4227
|
// Digests of the verdicts the report records, best-effort: an unreadable
|
|
4218
4228
|
// hint contributes no digest, which fails open to "outdated" rather than to
|
|
4219
4229
|
// a false match.
|
|
4220
|
-
function currentQaVerdictDigestsForReport(report, reportPath) {
|
|
4230
|
+
function currentQaVerdictDigestsForReport(report, reportPath, targetRepo) {
|
|
4221
4231
|
const digests = new Set();
|
|
4222
4232
|
// Lazy on purpose, and the check follows the add: once the limit is
|
|
4223
4233
|
// reached the next candidate is never pulled, read or hashed.
|
|
4224
|
-
for (const candidate of iterateQaVerdicts({ report, reportPath, withDigest: true })) {
|
|
4234
|
+
for (const candidate of iterateQaVerdicts({ report, reportPath, roots: [targetRepo], withDigest: true })) {
|
|
4225
4235
|
if (candidate.source === "assembly_report" && candidate.sha256) digests.add(candidate.sha256);
|
|
4226
4236
|
if (digests.size >= RUN_RECORD_QA_DIGEST_LIMIT) break;
|
|
4227
4237
|
}
|
|
@@ -4302,7 +4312,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4302
4312
|
// Atomic like the assembly report: a torn sidecar would be a corrupted
|
|
4303
4313
|
// freshness artifact — the exact green-lie shape this refresh exists to
|
|
4304
4314
|
// prevent (Kilo review, PR #176). Stamped generated_by: "next" (#312).
|
|
4305
|
-
writeDoctorSidecar(doctorOutPath, doctor, { command: NEXT_PRODUCER });
|
|
4315
|
+
writeDoctorSidecar(doctorOutPath, doctor, { command: NEXT_PRODUCER, targetRepo });
|
|
4306
4316
|
} catch {
|
|
4307
4317
|
// sidecar refresh is best-effort; orchestration must not fail on it
|
|
4308
4318
|
}
|
|
@@ -4318,7 +4328,10 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4318
4328
|
// participate in divergence detection or override prepare-only recovery.
|
|
4319
4329
|
const divergences = prepareBuildGate?.binding_failure
|
|
4320
4330
|
? []
|
|
4321
|
-
: detectLedgerDivergence(report, packet, targetRepo
|
|
4331
|
+
: detectLedgerDivergence(report, packet, targetRepo, {
|
|
4332
|
+
allowUnrecordedHostedPreview: polishCheckpointGate?.carried_forward?.policy === "hosted_template_preview"
|
|
4333
|
+
|| hostedTemplateNeedsPreviewUrl(packet, polishCheckpointGate),
|
|
4334
|
+
});
|
|
4322
4335
|
// Every return path runs through this finalizer so the machine-readable
|
|
4323
4336
|
// contract is uniform: `gates` (pass/blocked/waived/not_applicable per
|
|
4324
4337
|
// gate) and `next_actions` (exact commands — not prose) are always present,
|
|
@@ -4342,7 +4355,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4342
4355
|
records: readRunRecordsForTarget(dirname(packetPath)),
|
|
4343
4356
|
packet,
|
|
4344
4357
|
report,
|
|
4345
|
-
currentQaVerdictDigests: currentQaVerdictDigestsForReport(report, report ? reportPath : null),
|
|
4358
|
+
currentQaVerdictDigests: currentQaVerdictDigestsForReport(report, report ? reportPath : null, targetRepo),
|
|
4346
4359
|
qaVerdictRecorded: qaVerdictPathHints(report).length > 0,
|
|
4347
4360
|
});
|
|
4348
4361
|
} catch {
|
|
@@ -4452,7 +4465,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4452
4465
|
errors,
|
|
4453
4466
|
warnings,
|
|
4454
4467
|
ready,
|
|
4455
|
-
prompt:
|
|
4468
|
+
prompt: `${picked.outcome === "hosted_preview_satisfies_deploy" ? picked.reason : "Pipeline complete. All stages in the assembly report are in a terminal status."} To repeat build work, do the work and run \`${cmd("record")} build --packet <path>\`; this makes downstream stages owed as needed. Run \`${cmd("qa")} run\` for QA.${picked.outcome === "hosted_preview_satisfies_deploy" ? "" : " For a hosted deploy, record the URL and stage outcome as the deploy prompt describes."} Then call \`${cmd("next")}\` again. If a run session is active, finish it with \`${cmd("run")} end\` so the Run Record is assembled and the session closes.${report?.stages?.qa?.status === "completed_with_warnings" ? " QA passed with exceptions; report them to the operator without clearing or waiving them or changing markup just to make them pass." : ""}`,
|
|
4456
4469
|
});
|
|
4457
4470
|
}
|
|
4458
4471
|
stage = picked.stage;
|
|
@@ -4475,7 +4488,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4475
4488
|
if (!assemblyStatus.startsWith("completed")) addIssue(errors, "next.polish.assembly", `Assembly status is "${assemblyStatus || "missing"}"; polish expects completed assembly or an explicit blocked/skipped handoff.`);
|
|
4476
4489
|
if (polishGateRequiresBuild(polishGate)) addPolishGateErrors(errors, polishGate, "polish");
|
|
4477
4490
|
addThemeGateErrors(errors, themeGate, "polish");
|
|
4478
|
-
prompt = polishPrompt(packetPath, reportPath, packet, intentSummary);
|
|
4491
|
+
prompt = polishPrompt(packetPath, reportPath, packet, intentSummary, polishCheckpointGate);
|
|
4479
4492
|
} else if (stage === "deploy") {
|
|
4480
4493
|
addPrepareBuildGateErrors(errors, report);
|
|
4481
4494
|
// Slice 3 Phase 2: deploy is an out-of-band step (Netlify / CF Pages /
|
|
@@ -4485,7 +4498,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4485
4498
|
addPolishGateErrors(errors, polishGate, "deploy");
|
|
4486
4499
|
addPolishCheckpointGateErrors(errors, polishCheckpointGate, "deploy");
|
|
4487
4500
|
addThemeGateErrors(errors, themeGate, "deploy");
|
|
4488
|
-
prompt = deployPrompt(packetPath, reportPath, packet);
|
|
4501
|
+
prompt = deployPrompt(packetPath, reportPath, packet, polishGate, polishCheckpointGate);
|
|
4489
4502
|
} else if (stage === "qa") {
|
|
4490
4503
|
addPrepareBuildGateErrors(errors, report);
|
|
4491
4504
|
if (!report) addIssue(errors, "next.qa.report", "Assembly report is required before QA.");
|
|
@@ -4494,7 +4507,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4494
4507
|
addPolishGateErrors(errors, polishGate, "qa");
|
|
4495
4508
|
addPolishCheckpointGateErrors(errors, polishCheckpointGate, "qa");
|
|
4496
4509
|
addThemeGateErrors(errors, themeGate, "qa");
|
|
4497
|
-
prompt = qaPrompt(packetPath, reportPath, packet, spec, intentSummary);
|
|
4510
|
+
prompt = qaPrompt(packetPath, reportPath, packet, spec, intentSummary, polishCheckpointGate);
|
|
4498
4511
|
}
|
|
4499
4512
|
const status = errors.length
|
|
4500
4513
|
? "blocked"
|
|
@@ -4743,18 +4756,18 @@ function quoteDivergences(divergences) {
|
|
|
4743
4756
|
.join(" ");
|
|
4744
4757
|
}
|
|
4745
4758
|
|
|
4746
|
-
function divergenceInspectAction(divergences, packetPath) {
|
|
4759
|
+
function divergenceInspectAction(divergences, packetPath, { packet = null, polishCheckpointGate = null } = {}) {
|
|
4747
4760
|
const divergedStages = divergences.map((divergence) => divergence.stage);
|
|
4748
4761
|
const forwardHint = divergedStages.includes("qa")
|
|
4749
4762
|
? "The artifacts include a QA verdict for this campaign, so the campaign may already be built, deployed, and QA'd — verify the artifacts before redoing any stage."
|
|
4750
4763
|
: divergedStages.includes("deploy")
|
|
4751
|
-
? `If the recorded deploy URL is real and current, the forward path is QA (${cmd("next")} qa --packet ${packetPath}), not re-running earlier stages.`
|
|
4764
|
+
? `If the recorded deploy URL is real and current, the forward path is QA (${cmd("next")} qa --packet ${shellToken(packetPath)}), not re-running earlier stages.`
|
|
4752
4765
|
: "If the built output is real and current, the forward path is polish/deploy/QA, not re-running setup or build.";
|
|
4753
4766
|
return {
|
|
4754
4767
|
id: "divergence_inspect",
|
|
4755
4768
|
kind: "manual",
|
|
4756
4769
|
command: null,
|
|
4757
|
-
description: `Ledger and artifacts disagree — ${divergences.length} divergence(s): ${quoteDivergences(divergences)} The same entries are the divergences[] field of \`${cmd("next")} --json\` output; they are not written to any file. This is the ONLY next action: stage actions are suppressed while the disagreement stands, because every one of them would be derived from the same contradictory evidence. Inspect both sides and decide which is right; update the assembly report only after inspection. Do not rerun start/prepare-build or redo completed-looking work on the strength of the ledger alone, and do not treat artifact presence as proof a stage is complete. ${forwardHint} Re-run \`${cmd("next")} --packet ${packetPath} --json\` once the report matches the artifacts to get the normal action list.`,
|
|
4770
|
+
description: `Ledger and artifacts disagree — ${divergences.length} divergence(s): ${quoteDivergences(divergences)} The same entries are the divergences[] field of \`${cmd("next")} --json\` output; they are not written to any file. This is the ONLY next action: stage actions are suppressed while the disagreement stands, because every one of them would be derived from the same contradictory evidence. Inspect both sides and decide which is right; update the assembly report only after inspection. Do not rerun start/prepare-build or redo completed-looking work on the strength of the ledger alone, and do not treat artifact presence as proof a stage is complete. ${forwardHint}${hostedTemplateNeedsUsablePreview(packet, polishCheckpointGate) ? ` After reconciliation, set a hosted preview URL that differs from the production URL with \`${cmd("qa")} policy set --packet ${shellToken(packetPath)} --preview-url <url>\`.` : ""} Re-run \`${cmd("next")} --packet ${shellToken(packetPath)} --json\` once the report matches the artifacts to get the normal action list.`,
|
|
4758
4771
|
required: true,
|
|
4759
4772
|
};
|
|
4760
4773
|
}
|
|
@@ -4785,7 +4798,7 @@ export function buildNextActions({ result, packetPath, packet, themeGate, polish
|
|
|
4785
4798
|
// of the stale ledger. Suppressing branch-by-branch would leave the next
|
|
4786
4799
|
// branch someone adds unguarded; returning here cannot rot that way.
|
|
4787
4800
|
// The operator reconciles, then re-runs `next` for the normal list.
|
|
4788
|
-
const inspect = divergenceInspectAction(divergences, packetPath);
|
|
4801
|
+
const inspect = divergenceInspectAction(divergences, packetPath, { packet, polishCheckpointGate });
|
|
4789
4802
|
push(inspect.id, inspect.kind, asInvocation(inspect.command), inspect.description, { required: inspect.required });
|
|
4790
4803
|
return actions;
|
|
4791
4804
|
}
|
|
@@ -4811,7 +4824,7 @@ export function buildNextActions({ result, packetPath, packet, themeGate, polish
|
|
|
4811
4824
|
);
|
|
4812
4825
|
}
|
|
4813
4826
|
}
|
|
4814
|
-
push("doctor_recheck", "command", `${cmd("doctor")} --packet ${packetPath} --write --json`, "Record a fresh doctor result after resolving the listed errors.");
|
|
4827
|
+
push("doctor_recheck", "command", `${cmd("doctor")} --packet ${shellToken(packetPath)} --write --json`, "Record a fresh doctor result after resolving the listed errors.");
|
|
4815
4828
|
return actions;
|
|
4816
4829
|
}
|
|
4817
4830
|
if (result.stage === "prepare-build") {
|
|
@@ -4845,16 +4858,30 @@ export function buildNextActions({ result, packetPath, packet, themeGate, polish
|
|
|
4845
4858
|
for (const action of themeGate.required_actions) {
|
|
4846
4859
|
push(`theme_gate.${action.id}`, action.kind, asInvocation(action.command), action.description);
|
|
4847
4860
|
}
|
|
4848
|
-
push("recheck", "command", `${cmd("next")} --packet ${packetPath} --json`, "Re-run next after resolving the theme gate to advance.");
|
|
4861
|
+
push("recheck", "command", `${cmd("next")} --packet ${shellToken(packetPath)} --json`, "Re-run next after resolving the theme gate to advance.");
|
|
4849
4862
|
return actions;
|
|
4850
4863
|
}
|
|
4851
4864
|
if (polishGateRequiresBuild(polishGate) && ["build", "polish", "deploy", "qa"].includes(result.stage)) {
|
|
4852
4865
|
for (const action of polishGate?.required_actions || []) {
|
|
4853
4866
|
push(`polish_gate.${action.id}`, action.kind, asInvocation(action.command), action.description);
|
|
4854
4867
|
}
|
|
4855
|
-
push("recheck", "command", `${cmd("next")} --packet ${packetPath} --json`, "Re-run next after rebuilding against the current Design Source Package.");
|
|
4868
|
+
push("recheck", "command", `${cmd("next")} --packet ${shellToken(packetPath)} --json`, "Re-run next after rebuilding against the current Design Source Package.");
|
|
4856
4869
|
return actions;
|
|
4857
4870
|
}
|
|
4871
|
+
const hostedTemplate = packet?.deploy?.target && packet.deploy.target !== LOCAL_SERVE_DEPLOY_TARGET
|
|
4872
|
+
&& polishCheckpointGate?.code === "polish.hidden_eager_media.no_capturable_routes";
|
|
4873
|
+
if (hostedTemplate && ["polish", "deploy", "qa"].includes(result.stage)) {
|
|
4874
|
+
if (hostedTemplateNeedsUsablePreview(packet, polishCheckpointGate)) {
|
|
4875
|
+
pushPolishCheckpointActions();
|
|
4876
|
+
push("recheck", "command", `${cmd("next")} --packet ${shellToken(packetPath)} --json`, "Re-run next after recording a hosted preview URL that differs from the production URL.");
|
|
4877
|
+
return actions;
|
|
4878
|
+
}
|
|
4879
|
+
if (result.stage === "polish" && polishGate?.carried_forward?.policy === "hosted_template_preview") {
|
|
4880
|
+
push("next_qa", "command", `${cmd("next")} qa --packet ${shellToken(packetPath)}`, "Inspect QA for the hosted preview; missing Polish evidence remains a warning.");
|
|
4881
|
+
push("qa_run", "command", qaRunCommand(packetPath, packet.deploy.preview_url), "Run browser and typed-card QA against the hosted preview URL.");
|
|
4882
|
+
return actions;
|
|
4883
|
+
}
|
|
4884
|
+
}
|
|
4858
4885
|
if ((polishGate?.status === "blocked" || polishCheckpointGate?.status === "blocked")
|
|
4859
4886
|
&& ["deploy", "qa"].includes(result.stage)) {
|
|
4860
4887
|
const checkpointActionIds = new Set(
|
|
@@ -4867,7 +4894,7 @@ export function buildNextActions({ result, packetPath, packet, themeGate, polish
|
|
|
4867
4894
|
push(`polish_gate.${action.id}`, action.kind, asInvocation(action.command), action.description);
|
|
4868
4895
|
}
|
|
4869
4896
|
pushPolishCheckpointActions();
|
|
4870
|
-
push("recheck", "command", `${cmd("next")} --packet ${packetPath} --json`, "Re-run next after recording valid Polish evidence.");
|
|
4897
|
+
push("recheck", "command", `${cmd("next")} --packet ${shellToken(packetPath)} --json`, "Re-run next after recording valid Polish evidence.");
|
|
4871
4898
|
return actions;
|
|
4872
4899
|
}
|
|
4873
4900
|
// Ahead of every stage that still has QA in front of it, say what QA will do
|
|
@@ -4888,9 +4915,9 @@ export function buildNextActions({ result, packetPath, packet, themeGate, polish
|
|
|
4888
4915
|
}
|
|
4889
4916
|
}
|
|
4890
4917
|
if (result.stage === "setup") {
|
|
4891
|
-
push("setup_skill", "skill", "next-campaigns-os-setup", `Prepare the target page-kit structure and agent context, then record setup with ${cmd("record")} setup --packet ${packetPath}.`);
|
|
4918
|
+
push("setup_skill", "skill", "next-campaigns-os-setup", `Prepare the target page-kit structure and agent context, then record setup with ${cmd("record")} setup --packet ${shellToken(packetPath)}.`);
|
|
4892
4919
|
} else if (result.stage === "build") {
|
|
4893
|
-
const recordBuild = `${cmd("record")} build --packet ${packetPath}${isLocalServePacket(packet) ? ` --build-environment ${LOCAL_PROOF_BUILD_ENVIRONMENT}` : ""}`;
|
|
4920
|
+
const recordBuild = `${cmd("record")} build --packet ${shellToken(packetPath)}${isLocalServePacket(packet) ? ` --build-environment ${LOCAL_PROOF_BUILD_ENVIRONMENT}` : ""}`;
|
|
4894
4921
|
push("build_skill", "skill", "next-campaigns-build", `Assemble the campaign per the build prompt, run the page-kit build, then record build with ${recordBuild}.`);
|
|
4895
4922
|
if (isLocalServePacket(packet)) {
|
|
4896
4923
|
push("build_local_proof", "command", LOCAL_PROOF_BUILD_COMMAND, `Local proof mode (deploy.target is local-serve): build page-kit in the ${LOCAL_PROOF_BUILD_ENVIRONMENT} environment into _site/, then record it with ${recordBuild}, which sets ${LOCAL_PROOF_BUILD_ENVIRONMENT_FIELD} to "${LOCAL_PROOF_BUILD_ENVIRONMENT}" (never hand-edit it). Vendor loaders are environment-gated out of this render (their protocol-relative //host/... URLs fail over a plain-HTTP local serve); SDK dl_* events still fire. ${LOCAL_PROOF_NEVER_EDIT_RULE}`);
|
|
@@ -4902,16 +4929,25 @@ export function buildNextActions({ result, packetPath, packet, themeGate, polish
|
|
|
4902
4929
|
}
|
|
4903
4930
|
}
|
|
4904
4931
|
} else if (result.stage === "polish") {
|
|
4905
|
-
push("polish_skill", "skill", "next-campaigns-polish",
|
|
4932
|
+
push("polish_skill", "skill", "next-campaigns-polish", polishCheckpointGate?.code === "polish.hidden_eager_media.no_capturable_routes"
|
|
4933
|
+
? "Every mapped page is template stock, so there is no design route to capture. Follow the preview guidance for missing Polish evidence."
|
|
4934
|
+
: `Run the visual polish pass and ${cmd("polish")} capture, then record polish with ${cmd("record")} polish --packet ${shellToken(packetPath)} --evidence <polish-evidence.json>.`);
|
|
4906
4935
|
if (polishCheckpointGate?.status === "blocked") pushPolishCheckpointActions();
|
|
4907
4936
|
} else if (result.stage === "deploy") {
|
|
4908
|
-
if (packet.deploy?.
|
|
4937
|
+
if (polishGate?.carried_forward?.policy === "hosted_template_preview" && packet.deploy?.preview_url) {
|
|
4938
|
+
push("next_qa", "command", `${cmd("next")} qa --packet ${shellToken(packetPath)}`, "Inspect the QA stage for this hosted preview without recording a hosted deploy.");
|
|
4939
|
+
push("qa_run", "command", qaRunCommand(packetPath, packet.deploy.preview_url), "Run browser and typed-card QA against the hosted preview; its missing Polish evidence remains a warning.");
|
|
4940
|
+
} else if (packet.deploy?.target === LOCAL_SERVE_DEPLOY_TARGET) {
|
|
4909
4941
|
const plan = localServePlan(packet);
|
|
4910
|
-
push("deploy", "manual", null, `Serve the built ${plan.dir} output locally as the origin root (deploy.target is local-serve)${plan.rewrite ? ` — ${plan.rewrite}` : ""}, then run ${cmd("record")} deploy --packet ${packetPath} --base-url <served url>: it checks every built page answers and records deploy.preview_url and stages.deploy. Localhost on any port is a Development domain: SDK allowed, analytics suppressed.`);
|
|
4942
|
+
push("deploy", "manual", null, `Serve the built ${plan.dir} output locally as the origin root (deploy.target is local-serve)${plan.rewrite ? ` — ${plan.rewrite}` : ""}, then run ${cmd("record")} deploy --packet ${shellToken(packetPath)} --base-url <served url>: it checks every built page answers and records deploy.preview_url and stages.deploy. Localhost on any port is a Development domain: SDK allowed, analytics suppressed.`);
|
|
4911
4943
|
} else {
|
|
4912
4944
|
push("deploy", "manual", null, `Deploy _site/ output to ${packet.deploy?.target || "the deploy target"}, then record deploy.preview_url (or production_url) on the packet and stages.deploy in the assembly report.`);
|
|
4913
4945
|
}
|
|
4914
|
-
|
|
4946
|
+
if (polishGate?.carried_forward?.policy === "hosted_template_preview" && packet.deploy?.preview_url) {
|
|
4947
|
+
push("advance", "command", `${cmd("next")} qa --packet ${shellToken(packetPath)}`, "Inspect QA for the hosted preview without a deploy record.");
|
|
4948
|
+
} else {
|
|
4949
|
+
push("advance", "command", `${cmd("next")} --packet ${shellToken(packetPath)} --json`, "Advance to QA once the deploy URL is recorded.");
|
|
4950
|
+
}
|
|
4915
4951
|
} else if (result.stage === "qa") {
|
|
4916
4952
|
const url = packet.deploy?.preview_url || packet.deploy?.production_url || "<preview-url>";
|
|
4917
4953
|
push("install_browser", "command", `${cmd("qa")} install-browser`, "Install the Playwright browser once after install/update (npm run qa:install-browser from a checkout).");
|
|
@@ -5072,7 +5108,8 @@ Rules:
|
|
|
5072
5108
|
- For one-time prepurchase/order-bump packages outside the main bundles, default package_sync=false and show_line_total_price=false unless the spec explicitly requires quantity sync.
|
|
5073
5109
|
- Record spec-driven removals, especially unsupported payment methods, so polish does not reintroduce them.
|
|
5074
5110
|
- Replace demo refs; do not copy Olympus-style shipping_methods into shop-three-step.
|
|
5075
|
-
-
|
|
5111
|
+
- Multi-step checkout paths run select -> checkout_step... -> checkout; only the checkout page takes payment. On a select page the swap-mode bundle selector (data-next-bundle-selector with data-next-selection-mode="swap") writes the cart and the SDK persists it between pages; a checkout button or link (href from the page's next_url) goes to the next page. Use forcePackageId only on a page with no selector, such as a landing call-to-action into a checkout that renders none.
|
|
5112
|
+
- Each checkout_step page's checkout form (<form data-next-checkout="form">) carries data-next-checkout-step="<its next_url>" and data-next-step-number="<step_number>" (Build Context page_kit frontmatter step_number); the checkout page's form carries neither. On every page that feeds a Checkout (select, checkout_step, or a landing page with express checkout buttons), next-success-url is that Checkout's post-payment destination (frontmatter success_url), not the next step, because the SDK reads it for express orders; forward navigation uses the step form attribute or a plain link, never that meta tag.
|
|
5076
5113
|
- After page-kit build, inspect rendered _site output before handoff: each active page should have a body, Campaign Cart runtime markers, SDK meta tags from CampaignSpec sdk_hints.meta_tags, and no stale copied funnel attribution.
|
|
5077
5114
|
- Run page-kit build and SDK/template lint, then record build before polish: \`${cmd("record")} build --packet ${packetPath}${isLocalServePacket(packet) ? ` --build-environment ${LOCAL_PROOF_BUILD_ENVIRONMENT}` : ""} --adapter-decision <key>=<value>[,<key>=<value>...]\`. Record the value that is true for this build: for example, raw_html_conversion_status=completed once source HTML is converted, or not_required when there is none. Put all scalar adapter decisions in one comma-separated flag; a repeated flag keeps only the last. Use the allowed values in docs/build-packet.md. Record the nine recordable scalar decisions with the command, never by editing those fields in the Assembly Report. wrapper_policy is selected at intake with prepare-build --wrapper-policy or the source-html manifest's wrapper_policy option; the object-valued template_files_copied proof has no flag. It stamps stages.assembly.build_fingerprint with the fingerprint doctor computes from the built output (derived.build_output_fingerprint.value, sha256 over the sorted path+sha256 manifest of _site/<slug>/; doctor reports built_output.fingerprint_stale whenever the output on disk stops matching the recorded value), records report.design_source_package.material_fingerprint on stages.assembly.source_package_material_fingerprint when present, and sets stages.polish to "required" (required_by="build", required_for=["qa"]). Re-run it after every rebuild; never hand-edit these fields. Build must not mark stages.polish as completed/completed_with_warnings/skipped. If you applied a brand theme, run \`${cmd("record")} theme --packet ${packetPath}\` after record build: it reads each built commerce page's stylesheet links and records report.theme (status applied, load_order=after-next-core, css_path, commerce_pages, evidence), and refuses, writing nothing, when a page that loads next-core.css does not load the brand layer after it. Never hand-edit report.theme.
|
|
5078
5115
|
- Capture the machine-readable build summary as an artifact: \`${PAGE_KIT_BUILD_SUMMARY_CAPTURE_COMMAND}\` (requires next-campaign-page-kit >= 0.1.4). Doctor verifies it for per-page build errors and Page Kit shape warnings (NESTED_NO_PERMALINK, DUPLICATE_OUTPUT, MISSING_FRONTMATTER, LAYOUT_NOT_FOUND). If the installed page-kit predates --json, record that in the assembly report instead of skipping silently.${localProofPromptLines(packet, packetPath)}`;
|
|
@@ -5143,8 +5180,22 @@ When copying a starter template family, copy the family as an atomic page-kit sl
|
|
|
5143
5180
|
Do not wire checkout, upsell, receipt, payment, package, voucher, or shipping behavior during setup.`;
|
|
5144
5181
|
}
|
|
5145
5182
|
|
|
5146
|
-
function polishPrompt(packetPath, reportPath, packet, intent) {
|
|
5183
|
+
function polishPrompt(packetPath, reportPath, packet, intent, polishCheckpointGate = null) {
|
|
5147
5184
|
const briefPath = packet.build_brief?.normalized_path || "(missing)";
|
|
5185
|
+
if (polishCheckpointGate?.code === "polish.hidden_eager_media.no_capturable_routes") {
|
|
5186
|
+
if (packet.deploy?.target === LOCAL_SERVE_DEPLOY_TARGET && polishCheckpointGate?.status !== "carried_forward") {
|
|
5187
|
+
return `Every mapped page is template stock, so there is no design route to capture. Missing Polish evidence is carried forward only on the local preview served from a loopback host (localhost, 127.0.0.1 or [::1]); this packet's deploy.preview_url is not one. Serve the build locally and record it with \`${cmd("record")} deploy --packet ${shellToken(packetPath)} --base-url <served url>\`, then run \`${cmd("next")} --packet ${shellToken(packetPath)}\`.`;
|
|
5188
|
+
}
|
|
5189
|
+
if (packet.deploy?.target === LOCAL_SERVE_DEPLOY_TARGET) {
|
|
5190
|
+
return `Every mapped page is template stock, so there is no design route to capture. On the local loopback preview, missing Polish evidence is carried forward as a warning. Run \`${cmd("next")} --packet ${shellToken(packetPath)}\` to continue to deploy and QA. Production QA still requires the evidence.`;
|
|
5191
|
+
}
|
|
5192
|
+
if (polishCheckpointGate?.status !== "carried_forward" && packet.deploy?.preview_url) {
|
|
5193
|
+
return `Every mapped page is template stock, so there is no design route to capture. Missing Polish evidence is carried forward only on a hosted preview whose deploy.preview_url is a non-loopback http(s) URL different from deploy.production_url; this packet's is not. Set one with \`${cmd("qa")} policy set --packet ${shellToken(packetPath)} --preview-url <url>\`, then \`${cmd("next")} --packet ${shellToken(packetPath)}\`.`;
|
|
5194
|
+
}
|
|
5195
|
+
return packet.deploy?.preview_url
|
|
5196
|
+
? `Every mapped page is template stock, so there is no design route to capture. Missing Polish evidence is carried forward on the hosted preview. Run \`${cmd("next")} qa --packet ${shellToken(packetPath)}\`, then \`${qaRunCommand(packetPath, packet.deploy.preview_url)}\`. Production QA remains blocked without the evidence.`
|
|
5197
|
+
: `Every mapped page is template stock, so there is no design route to capture. Run \`${cmd("qa")} policy set --packet ${shellToken(packetPath)} --preview-url <url>\` to record the hosted preview URL, then \`${cmd("next")} --packet ${shellToken(packetPath)}\`.`;
|
|
5198
|
+
}
|
|
5148
5199
|
return `${campaignIntentPromptHeader(intent)}Use next-campaigns-polish for this built campaign.
|
|
5149
5200
|
|
|
5150
5201
|
Read first:
|
|
@@ -5203,7 +5254,12 @@ function localServePlan(packet) {
|
|
|
5203
5254
|
};
|
|
5204
5255
|
}
|
|
5205
5256
|
|
|
5206
|
-
function deployPrompt(packetPath, reportPath, packet) {
|
|
5257
|
+
function deployPrompt(packetPath, reportPath, packet, polishGate = null, polishCheckpointGate = null) {
|
|
5258
|
+
if (packet.deploy?.target !== LOCAL_SERVE_DEPLOY_TARGET
|
|
5259
|
+
&& polishCheckpointGate?.code === "polish.hidden_eager_media.no_capturable_routes"
|
|
5260
|
+
&& !packet.deploy?.preview_url) {
|
|
5261
|
+
return `Every mapped page is template stock, and the hosted preview URL is not recorded. Run \`${cmd("qa")} policy set --packet ${shellToken(packetPath)} --preview-url <url>\` after the preview is deployed, then \`${cmd("next")} --packet ${shellToken(packetPath)}\`. There is no hosted record deploy command.`;
|
|
5262
|
+
}
|
|
5207
5263
|
const target = packet.deploy?.target || "unknown";
|
|
5208
5264
|
const liveUrlPath = packet.deploy?.live_url_path || packet.campaign?.live_url_path || campaignRouteRoot(packet) || "/<slug>/";
|
|
5209
5265
|
if (target === LOCAL_SERVE_DEPLOY_TARGET) {
|
|
@@ -5225,6 +5281,9 @@ Once the server is up:
|
|
|
5225
5281
|
|
|
5226
5282
|
If the served build cannot be reached, set stages.deploy.status to "blocked" with a clear reason in outputs so the orchestration loop surfaces it rather than skipping past.`;
|
|
5227
5283
|
}
|
|
5284
|
+
if (polishGate?.carried_forward?.policy === "hosted_template_preview" && packet.deploy?.preview_url) {
|
|
5285
|
+
return `The hosted preview URL is recorded at deploy.preview_url. Every mapped page is template stock, so missing Polish evidence is carried forward as a warning for this preview. Run \`${cmd("next")} qa --packet ${shellToken(packetPath)}\` to inspect the QA stage, then \`${qaRunCommand(packetPath, packet.deploy.preview_url)}\` to test the preview. The recorded hosted preview satisfies this deploy handoff; after QA records its verdict, run \`${cmd("next")} --packet ${shellToken(packetPath)}\` for closeout. Production QA remains strict.`;
|
|
5286
|
+
}
|
|
5228
5287
|
return `Deploy the built campaign to ${target}.
|
|
5229
5288
|
|
|
5230
5289
|
Read first:
|
|
@@ -5247,36 +5306,40 @@ If the deploy is blocked (non-localhost allowed-domain not yet added, CI permiss
|
|
|
5247
5306
|
// The QA command the QA stage hands over. Given a bump cart it is the
|
|
5248
5307
|
// order-bump run: the same command with --cart selecting the base tier and
|
|
5249
5308
|
// toggling each declared bump.
|
|
5250
|
-
function qaRunCommand(packetPath, url, bumpCart = null) {
|
|
5251
|
-
return `${cmd("qa")} run --packet ${packetPath} --base-url ${url} --browser --test-order common${bumpCart ? ` --cart ${shellToken(bumpCart.cart)}` : ""}`;
|
|
5252
|
-
}
|
|
5253
|
-
|
|
5254
5309
|
// `qa run --test-order common` never puts a checkout order bump in a test
|
|
5255
5310
|
// order: the tier planner skips bump rows by design and bump coverage comes
|
|
5256
5311
|
// from --cart. So when the spec declares one, `next` names a second run with
|
|
5257
|
-
// the bump in the cart. It reads the one checkout QA drives, with QA's own
|
|
5258
|
-
// classifiers: QA's test orders run on
|
|
5259
|
-
//
|
|
5260
|
-
// to enabled and sorted by `order || 0`
|
|
5261
|
-
//
|
|
5262
|
-
//
|
|
5263
|
-
//
|
|
5264
|
-
//
|
|
5265
|
-
//
|
|
5266
|
-
//
|
|
5312
|
+
// the bump in the cart. It reads the one checkout path QA drives, with QA's own
|
|
5313
|
+
// row classifiers: QA's test orders run on the Checkout (the page that takes
|
|
5314
|
+
// payment) of the first entry path of the first funnel that has one, each
|
|
5315
|
+
// funnel's pages filtered to enabled and sorted by `order || 0`
|
|
5316
|
+
// (extractTopologies, findPaymentPage). The rows are read from every
|
|
5317
|
+
// checkout-flow page on that path — the select page or first step of a
|
|
5318
|
+
// multi-step checkout declares the cart, not the Checkout (campaigns-os#641).
|
|
5319
|
+
// When that path declares no bump it returns null rather than looking at a
|
|
5320
|
+
// later funnel: QA never drives a later funnel's checkout, so that funnel's
|
|
5321
|
+
// bump ref in --cart would land on a checkout without it. The base is the
|
|
5322
|
+
// first selector tier QA would plan: QA's default keeps the page's
|
|
5323
|
+
// pre-selected card, which a spec does not name. A path that declares no tier
|
|
5324
|
+
// (the cart is filled on an entry page) gets the bump alone, on whatever
|
|
5325
|
+
// selection the entry page made.
|
|
5267
5326
|
export function checkoutOrderBumpCart(spec) {
|
|
5268
|
-
const
|
|
5269
|
-
|
|
5270
|
-
|
|
5327
|
+
const upgraded = upgradeCampaignSpec(spec);
|
|
5328
|
+
const funnels = Array.isArray(upgraded?.funnels)
|
|
5329
|
+
? upgraded.funnels
|
|
5330
|
+
: Array.isArray(upgraded?.funnel_pages) ? [{ pages: upgraded.funnel_pages }] : [];
|
|
5271
5331
|
for (const funnel of funnels) {
|
|
5272
|
-
const
|
|
5332
|
+
const pages = (Array.isArray(funnel?.pages) ? funnel.pages : [])
|
|
5273
5333
|
.filter((page) => page && page.enabled !== false)
|
|
5274
|
-
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
|
5275
|
-
|
|
5334
|
+
.sort((a, b) => (a.order || 0) - (b.order || 0));
|
|
5335
|
+
const sorted = { ...funnel, pages };
|
|
5336
|
+
const checkout = paymentPageForFunnel(sorted);
|
|
5276
5337
|
if (!checkout) continue;
|
|
5277
|
-
const
|
|
5338
|
+
const entryPath = entryPages(sorted).map((entry) => checkoutPathFrom(pages, entry)).find((path) => path?.includes(checkout));
|
|
5339
|
+
const pathPages = (entryPath || [checkout]).filter((page) => isCheckoutFlowPage(page));
|
|
5340
|
+
const bumps = [...new Set(pathPages.flatMap((page) => declaredOrderBumps(page)))];
|
|
5278
5341
|
if (!bumps.length) return null;
|
|
5279
|
-
const base = declaredSelectorTiers(
|
|
5342
|
+
const base = pathPages.map((page) => declaredSelectorTiers(page)[0]?.ref).find(Boolean) || null;
|
|
5280
5343
|
return { base, bumps, cart: [base, ...bumps].filter(Boolean).map((ref) => `${ref}:1`).join(",") };
|
|
5281
5344
|
}
|
|
5282
5345
|
return null;
|
|
@@ -5288,7 +5351,12 @@ function orderBumpRunReason(bumpCart) {
|
|
|
5288
5351
|
return `The checkout declares order bump ${bumpCart.bumps.join(", ")} (is_order_bump or is_upsell), and the default run never puts it in a test order. This run repeats QA with ${cart} in the cart.`;
|
|
5289
5352
|
}
|
|
5290
5353
|
|
|
5291
|
-
function qaPrompt(packetPath, reportPath, packet, spec = null, intent) {
|
|
5354
|
+
function qaPrompt(packetPath, reportPath, packet, spec = null, intent, polishCheckpointGate = null) {
|
|
5355
|
+
if (packet.deploy?.target !== LOCAL_SERVE_DEPLOY_TARGET
|
|
5356
|
+
&& polishCheckpointGate?.code === "polish.hidden_eager_media.no_capturable_routes"
|
|
5357
|
+
&& !packet.deploy?.preview_url) {
|
|
5358
|
+
return `Every mapped page is template stock, and QA needs the hosted preview URL. Run \`${cmd("qa")} policy set --packet ${shellToken(packetPath)} --preview-url <url>\`, then \`${cmd("next")} qa --packet ${shellToken(packetPath)}\`.`;
|
|
5359
|
+
}
|
|
5292
5360
|
const url = packet.deploy?.preview_url || packet.deploy?.production_url || "<preview-url>";
|
|
5293
5361
|
const briefPath = packet.build_brief?.normalized_path || "(missing)";
|
|
5294
5362
|
const bumpCart = checkoutOrderBumpCart(spec);
|
|
@@ -5356,7 +5424,7 @@ function qaVerdictArtifactsForCampaign(packet, targetRepo) {
|
|
|
5356
5424
|
// Presence-based only: a stale built output still diverges from a pending
|
|
5357
5425
|
// ledger, and the entry says the two disagree — it never asserts the stage
|
|
5358
5426
|
// is complete or the artifact current. This function writes nothing.
|
|
5359
|
-
export function detectLedgerDivergence(report, packet, targetRepo) {
|
|
5427
|
+
export function detectLedgerDivergence(report, packet, targetRepo, { allowUnrecordedHostedPreview = false } = {}) {
|
|
5360
5428
|
const divergences = [];
|
|
5361
5429
|
const stages = report?.stages;
|
|
5362
5430
|
if (!isObject(stages) || !isNonEmptyString(targetRepo)) return divergences;
|
|
@@ -5389,7 +5457,7 @@ export function detectLedgerDivergence(report, packet, targetRepo) {
|
|
|
5389
5457
|
const packetPreviewUrl = optionalString(packet?.deploy?.preview_url);
|
|
5390
5458
|
const packetProductionUrl = optionalString(packet?.deploy?.production_url);
|
|
5391
5459
|
const url = reportUrl || packetPreviewUrl || packetProductionUrl;
|
|
5392
|
-
if (url) {
|
|
5460
|
+
if (url && !(allowUnrecordedHostedPreview && !reportUrl)) {
|
|
5393
5461
|
const source = reportUrl
|
|
5394
5462
|
? "report.stages.deploy.outputs"
|
|
5395
5463
|
: packetPreviewUrl
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { specHashOf, specHashesMatch } from "./spec-hash.mjs";
|
|
14
|
+
import { CHECKOUT_FLOW_PAGE_TYPES, upgradeCampaignSpec } from "../campaign-spec/dist/index.js";
|
|
14
15
|
|
|
15
16
|
export const CALC_LABEL = "Campaigns-calculated · before tax";
|
|
16
17
|
export const CALCULATED_PAIR_EVIDENCE = "calculated_pair";
|
|
@@ -213,6 +214,20 @@ function catalogImportedAt(mapDoc) {
|
|
|
213
214
|
?? null;
|
|
214
215
|
}
|
|
215
216
|
|
|
217
|
+
// The page's type in the v5 vocabulary: a v4 Map's `checkout → checkout` chain
|
|
218
|
+
// reads as `checkout_step` pages ahead of the one Checkout (campaigns-os#641).
|
|
219
|
+
const upgradedMaps = new WeakMap();
|
|
220
|
+
function v5PageType(mapDoc, page) {
|
|
221
|
+
if (!mapDoc || typeof mapDoc !== "object") return page?.type ?? null;
|
|
222
|
+
if (!upgradedMaps.has(mapDoc)) upgradedMaps.set(mapDoc, upgradeCampaignSpec(mapDoc));
|
|
223
|
+
const upgraded = upgradedMaps.get(mapDoc);
|
|
224
|
+
for (const funnel of array(upgraded?.funnels)) {
|
|
225
|
+
const hit = array(funnel?.pages).find((candidate) => present(page?.id) && String(candidate?.id) === String(page.id));
|
|
226
|
+
if (hit) return hit.type ?? null;
|
|
227
|
+
}
|
|
228
|
+
return page?.type ?? null;
|
|
229
|
+
}
|
|
230
|
+
|
|
216
231
|
function descriptor(page, mapDoc, role, lines, options = {}) {
|
|
217
232
|
const funnel = findFunnel(mapDoc, page);
|
|
218
233
|
const suffix = options.id_suffix ? `:${options.id_suffix}` : "";
|
|
@@ -221,7 +236,7 @@ function descriptor(page, mapDoc, role, lines, options = {}) {
|
|
|
221
236
|
upsell: page?.type === "upsell" || page?.type === "downsell",
|
|
222
237
|
context: {
|
|
223
238
|
page_id: page?.id ?? null,
|
|
224
|
-
page_type: page
|
|
239
|
+
page_type: v5PageType(mapDoc, page),
|
|
225
240
|
page_label: page?.label ?? page?.name ?? page?.id ?? null,
|
|
226
241
|
page_order: Number(page?.order) || 0,
|
|
227
242
|
funnel_id: funnel?.id ?? null,
|
|
@@ -1045,8 +1060,16 @@ function acceptDelta(page) {
|
|
|
1045
1060
|
return moneyFact(signedCents(cents), total.state);
|
|
1046
1061
|
}
|
|
1047
1062
|
|
|
1063
|
+
// The checkout total is the cart the shopper pays for at the Checkout. That cart
|
|
1064
|
+
// is priced on whichever checkout-flow page carries it: the select page of a
|
|
1065
|
+
// two-step flow, the first step of a split checkout, or the Checkout itself
|
|
1066
|
+
// (campaigns-os#641). The Checkout of a multi-step path carries no packages, so
|
|
1067
|
+
// looking for the page typed "checkout" priced nothing on those flows. Only
|
|
1068
|
+
// pages that declare packages are planned, so every page here carries a cart;
|
|
1069
|
+
// pages arrive sorted by page_order, and with several paths the first is
|
|
1070
|
+
// summarized.
|
|
1048
1071
|
function journeySummary(pages) {
|
|
1049
|
-
const checkout = pages.find((page) => page.page_type
|
|
1072
|
+
const checkout = pages.find((page) => CHECKOUT_FLOW_PAGE_TYPES.includes(page.page_type));
|
|
1050
1073
|
const checkoutTotal = checkout?.representative_total
|
|
1051
1074
|
|| unresolvedFact("Unresolved: checkout scenario unavailable");
|
|
1052
1075
|
const bumps = pages.flatMap((page) => page.bumps.map((bump) => ({ ...bump, page_id: page.page_id })));
|
|
@@ -2085,7 +2085,7 @@ function sourceReferenceKindForManifestFile(file) {
|
|
|
2085
2085
|
function normalizedPageRole(type) {
|
|
2086
2086
|
const normalized = slugify(type || "page");
|
|
2087
2087
|
if (["landing", "product", "presell", "advertorial"].includes(normalized)) return "landing";
|
|
2088
|
-
if (["checkout", "select", "cart"].includes(normalized)) return "checkout";
|
|
2088
|
+
if (["checkout", "checkout-step", "checkout_step", "select", "cart"].includes(normalized)) return "checkout";
|
|
2089
2089
|
if (["upsell", "oto", "one-time-offer"].includes(normalized)) return "upsell";
|
|
2090
2090
|
if (["downsell", "downsell-offer"].includes(normalized)) return "downsell";
|
|
2091
2091
|
if (["receipt", "thank-you", "thankyou", "confirmation"].includes(normalized)) return "receipt";
|
|
@@ -4,7 +4,7 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { accessSync, constants as fsConstants, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { writeThemeArtifacts } from "./brand-theme.mjs";
|
|
7
|
-
import { cloneJson, filesystemPathsMatch, isObject, isNonEmptyString, optionalString, readJson, resolveFromFile, sha256File } from "./cli-helpers.mjs";
|
|
7
|
+
import { cloneJson, filesystemPathsMatch, isObject, isNonEmptyString, optionalString, portableArtifactPaths, readJson, resolveFromFile, sha256File } from "./cli-helpers.mjs";
|
|
8
8
|
import { DESIGN_SOURCE_PACKAGE_REL_PATH, createDesignSourcePackageArtifactReference, hashSerializedDesignSourcePackage, serializeDesignSourcePackage, synthesizeHtmlFunnelDesignSourcePackage, validateDesignSourcePackage } from "./design-source-package.mjs";
|
|
9
9
|
import { stampDoctorProducer } from "./doctor-sidecar.mjs";
|
|
10
10
|
import { stageWriteInputs } from "./input-currency.mjs";
|
|
@@ -21,7 +21,7 @@ import { resolveTemplateFamilyDesignSource } from "./template-reference.mjs";
|
|
|
21
21
|
// states never count as accumulated agent evidence. setup's seed state may
|
|
22
22
|
// legitimately be "skipped" (scaffold already present); every other stage is
|
|
23
23
|
// seeded "pending" with empty ledger arrays.
|
|
24
|
-
function assemblyReportStagesWithEvidence(existingReport) {
|
|
24
|
+
export function assemblyReportStagesWithEvidence(existingReport) {
|
|
25
25
|
const stages = existingReport?.stages;
|
|
26
26
|
if (!isObject(stages)) return [];
|
|
27
27
|
const withEvidence = [];
|
|
@@ -979,10 +979,10 @@ export async function withDesignSourcePublication({ targetRepo, outputs, force =
|
|
|
979
979
|
: [],
|
|
980
980
|
);
|
|
981
981
|
publishPrepareBuildJsonOutputs([
|
|
982
|
-
{ label: "Build Packet", path: packetPath, value: packet },
|
|
983
|
-
{ label: "Campaign Build Brief", path: briefPath, value: brief },
|
|
984
|
-
{ label: "Build Context", path: contextPath, value: context },
|
|
985
|
-
{ label: "Assembly Report", path: reportPath, value: report },
|
|
982
|
+
{ label: "Build Packet", path: packetPath, value: portableArtifactPaths(packet, targetRepo, { artifactPath: packetPath }) },
|
|
983
|
+
{ label: "Campaign Build Brief", path: briefPath, value: portableArtifactPaths(brief, targetRepo, { artifactPath: briefPath }) },
|
|
984
|
+
{ label: "Build Context", path: contextPath, value: portableArtifactPaths(context, targetRepo, { artifactPath: contextPath }) },
|
|
985
|
+
{ label: "Assembly Report", path: reportPath, value: portableArtifactPaths(report, targetRepo, { artifactPath: reportPath }) },
|
|
986
986
|
], prepareBuildCollisionPaths, {
|
|
987
987
|
beforePublish: recheckStageEvidence,
|
|
988
988
|
});
|
|
@@ -997,7 +997,7 @@ export async function withDesignSourcePublication({ targetRepo, outputs, force =
|
|
|
997
997
|
// stamped with the intake command that ran doctor (`start` or `build`,
|
|
998
998
|
// #312) without a second write path for it.
|
|
999
999
|
publishPrepareBuildJsonOutputs([
|
|
1000
|
-
{ label: "Doctor Output", path: doctorOutPath, value: stampDoctorProducer(doctor, command) },
|
|
1000
|
+
{ label: "Doctor Output", path: doctorOutPath, value: portableArtifactPaths(stampDoctorProducer(doctor, command), targetRepo) },
|
|
1001
1001
|
], prepareBuildCollisionPaths);
|
|
1002
1002
|
},
|
|
1003
1003
|
}));
|