@nextcommerce/campaigns-os 1.55.0 → 1.59.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 +124 -0
- package/CONTEXT.md +11 -0
- package/campaign-spec/README.md +15 -1
- package/campaign-spec/dist/checkout-flow.d.ts +97 -0
- package/campaign-spec/dist/checkout-flow.js +245 -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/campaign-cart-sdk-support-policy.v0.json +2 -2
- package/contracts/commerce-surface-catalog.json +26 -27
- package/contracts/effects.v1.json +5 -5
- 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 +1137 -0
- package/contracts/runtime-recipe.campaigns-os-node-v1.json +6 -1
- package/contracts/supported-surface.json +9 -5
- package/contracts/template-brand-contract.shared-commerce.v0.json +1 -1
- package/docs/build-packet.md +68 -10
- package/docs/local-setup.md +1 -1
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/qa-and-test-orders.md +124 -22
- 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 +4 -1
- package/schemas/campaign-runtime-build-packet.v0.schema.json +4 -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/built-site-scope.mjs +91 -0
- package/src/cli-helpers.mjs +129 -0
- package/src/cli.mjs +117 -57
- 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 +269 -16
- package/src/doctor/inspect.mjs +4 -2
- package/src/doctor/next-step.mjs +18 -9
- 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 +8 -7
- package/src/qa-browser.mjs +363 -36
- package/src/qa-cart-entry.mjs +183 -14
- package/src/qa-node.mjs +38 -34
- package/src/qa-test-order-topology.mjs +118 -1
- package/src/qa-verdict-discovery.mjs +21 -4
- package/src/qa-verdict.mjs +24 -1
- package/src/runtime-state-ignore.mjs +7 -5
- package/src/sdk-attribute-index.mjs +8 -7
- package/src/source-html-intake.mjs +58 -7
- package/src/stage-ledger.mjs +4 -2
- package/src/stage-record.mjs +203 -36
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>]
|
|
@@ -339,7 +343,7 @@ Usage:
|
|
|
339
343
|
campaigns-os record build --packet <campaign-runtime.build.json> [--build-environment <development|production>] [--adapter-decision <key>=<value>[,<key>=<value>...]] [--context <json>] [--report <json>] [--dry-run] [--json] # after page-kit build: records scalar adapter decisions on the Assembly Report (all pairs in one flag; a repeated flag keeps only the last), stamps stages.assembly.build_fingerprint from doctor's derived.build_output_fingerprint.value (and the Design Source Package material fingerprint when present), and makes Polish required unless its evidence is bound to this output; --build-environment records stages.assembly.evidence.build_environment (local proof mode records development)
|
|
340
344
|
campaigns-os record polish --packet <campaign-runtime.build.json> --evidence <polish-evidence.json> [--context <json>] [--report <json>] [--dry-run] [--json] # after polish capture: stages.polish from the file's status (completed, completed_with_warnings, blocked with blockers, or skipped with skip_reason), evidence and optional repair_loop_defect, bound to doctor's current fingerprint; a completed status is refused, writing nothing, unless the polish gate doctor evaluates would pass. --dry-run runs every check and writes nothing
|
|
341
345
|
campaigns-os record theme --packet <campaign-runtime.build.json> [--context <json>] [--report <json>] [--dry-run] [--json] # after the brand layer is linked and build is recorded: report.theme becomes applied with load_order after-next-core, css_path, commerce_pages and per-page evidence, only when each built commerce page that loads next-core.css loads brand-theme.css (or checkout-brand.css) after it and at least one does; a page loading neither is left out as the design's own markup; refused, writing nothing, otherwise. --dry-run runs every check and writes nothing
|
|
342
|
-
campaigns-os record deploy --packet <campaign-runtime.build.json> --base-url <served url> [--context <json>] [--report <json>] [--dry-run] [--json] # a
|
|
346
|
+
campaigns-os record deploy --packet <campaign-runtime.build.json> --base-url <served url> [--context <json>] [--report <json>] [--dry-run] [--json] # a served deploy after polish is recorded: GETs every built page under the URL (the campaign route root), then records deploy.preview_url on the packet and stages.deploy completed with the URL in outputs and the build fingerprint it probed (a later record build of other output makes deploy required again). A local-serve packet takes a loopback URL and each page must answer 2xx; any other deploy target takes an https URL (plain http only on a loopback host), each page must answer 200, and every script and stylesheet the built pages load from that origin must be served byte-identical (sha256) to the built output (HTML is not compared). Refused, writing nothing, when the URL breaks those rules or is not the route root, a page does not answer, a served asset differs or is missing, the build changed since it was recorded, or the theme gate is blocked. --dry-run runs every check, the requests included, and writes nothing
|
|
343
347
|
campaigns-os readback <target-repo-root> [--json] [--packet <path>] [--doctor <path>] [--context <path>] [--report <path>] [--qa-verdict <path>] [--findings <path>] # read-only projection of one run's emitted artifacts (packet, doctor output, build context, assembly report, QA verdict, findings export): artifact states, per-artifact freshness against the checkout's HEAD reflog, doctor warning grouping, skip cascades and cross-artifact divergences. Writes nothing, starts no process, touches no network, and records no lifecycle entry; --json emits one campaigns-os-readback/v2 object (docs/readback.md). Exit 2 for a missing target root or a Build Packet set freshness cannot single out.
|
|
344
348
|
campaigns-os readback --example [--json] # project the bundled synthetic sample; freshness is not computable for it by design
|
|
345
349
|
campaigns-os validate-assembly-report --report <json> [--json]
|
|
@@ -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 {
|
|
@@ -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("record_deploy", "command", `${cmd("record")} deploy --packet ${shellToken(packetPath)} --base-url ${shellToken(packet.deploy.preview_url)}`, "Record the hosted preview deploy; missing Polish evidence remains a warning.");
|
|
4881
|
+
push("qa_run", "command", qaRunCommand(packetPath, packet.deploy.preview_url), "Then 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,18 @@ 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
4937
|
if (packet.deploy?.target === LOCAL_SERVE_DEPLOY_TARGET) {
|
|
4909
4938
|
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.`);
|
|
4939
|
+
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
4940
|
} else {
|
|
4912
|
-
push("deploy", "manual", null, `Deploy _site/ output to ${packet.deploy?.target || "the deploy target"}, then record deploy.preview_url (
|
|
4941
|
+
push("deploy", "manual", null, `Deploy _site/ output to ${packet.deploy?.target || "the deploy target"} with _site/ as the origin root, then run ${cmd("record")} deploy --packet ${shellToken(packetPath)} --base-url ${packet.deploy?.preview_url ? shellToken(packet.deploy.preview_url) : "<https preview url>"}: it checks every built page answers HTTP 200 and every script and stylesheet the pages load from that origin is served byte-identical to the build, and records deploy.preview_url and stages.deploy.`);
|
|
4913
4942
|
}
|
|
4914
|
-
push("advance", "command", `${cmd("next")} --packet ${packetPath} --json`, "Advance to QA once the deploy URL is recorded.");
|
|
4943
|
+
push("advance", "command", `${cmd("next")} --packet ${shellToken(packetPath)} --json`, "Advance to QA once the deploy URL is recorded.");
|
|
4915
4944
|
} else if (result.stage === "qa") {
|
|
4916
4945
|
const url = packet.deploy?.preview_url || packet.deploy?.production_url || "<preview-url>";
|
|
4917
4946
|
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 +5101,8 @@ Rules:
|
|
|
5072
5101
|
- 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
5102
|
- Record spec-driven removals, especially unsupported payment methods, so polish does not reintroduce them.
|
|
5074
5103
|
- Replace demo refs; do not copy Olympus-style shipping_methods into shop-three-step.
|
|
5075
|
-
-
|
|
5104
|
+
- 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_step) 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.
|
|
5105
|
+
- Each checkout_step page's checkout form (<form data-next-checkout="form">) carries data-next-checkout-step="<its next_step>" and data-next-step-number="<step_number>" (Build Context page_kit frontmatter next_step and step_number); the checkout page's form carries neither. On select and checkout_step pages, frontmatter next_url is the Checkout's post-payment destination, the same meaning it has on the checkout page, and the layout renders it as next-success-url because the SDK reads that tag for express orders; next_step is the forward link. A landing page keeps next_url as its call-to-action target; if it has express checkout buttons, set its next-success-url to the Checkout's own next_url from the Build Context page_map, never to the landing page's next_url. Forward navigation uses the step form attribute or a plain link, never that meta tag. Page Kit frontmatter has no success_url key (success_url remains a CampaignSpec field on checkout pages).
|
|
5076
5106
|
- 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
5107
|
- 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
5108
|
- 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 +5173,22 @@ When copying a starter template family, copy the family as an atomic page-kit sl
|
|
|
5143
5173
|
Do not wire checkout, upsell, receipt, payment, package, voucher, or shipping behavior during setup.`;
|
|
5144
5174
|
}
|
|
5145
5175
|
|
|
5146
|
-
function polishPrompt(packetPath, reportPath, packet, intent) {
|
|
5176
|
+
function polishPrompt(packetPath, reportPath, packet, intent, polishCheckpointGate = null) {
|
|
5147
5177
|
const briefPath = packet.build_brief?.normalized_path || "(missing)";
|
|
5178
|
+
if (polishCheckpointGate?.code === "polish.hidden_eager_media.no_capturable_routes") {
|
|
5179
|
+
if (packet.deploy?.target === LOCAL_SERVE_DEPLOY_TARGET && polishCheckpointGate?.status !== "carried_forward") {
|
|
5180
|
+
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)}\`.`;
|
|
5181
|
+
}
|
|
5182
|
+
if (packet.deploy?.target === LOCAL_SERVE_DEPLOY_TARGET) {
|
|
5183
|
+
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.`;
|
|
5184
|
+
}
|
|
5185
|
+
if (polishCheckpointGate?.status !== "carried_forward" && packet.deploy?.preview_url) {
|
|
5186
|
+
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)}\`.`;
|
|
5187
|
+
}
|
|
5188
|
+
return packet.deploy?.preview_url
|
|
5189
|
+
? `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("record")} deploy --packet ${shellToken(packetPath)} --base-url ${shellToken(packet.deploy.preview_url)}\`, then \`${qaRunCommand(packetPath, packet.deploy.preview_url)}\`. Production QA remains blocked without the evidence.`
|
|
5190
|
+
: `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)}\`.`;
|
|
5191
|
+
}
|
|
5148
5192
|
return `${campaignIntentPromptHeader(intent)}Use next-campaigns-polish for this built campaign.
|
|
5149
5193
|
|
|
5150
5194
|
Read first:
|
|
@@ -5203,7 +5247,12 @@ function localServePlan(packet) {
|
|
|
5203
5247
|
};
|
|
5204
5248
|
}
|
|
5205
5249
|
|
|
5206
|
-
function deployPrompt(packetPath, reportPath, packet) {
|
|
5250
|
+
function deployPrompt(packetPath, reportPath, packet, polishGate = null, polishCheckpointGate = null) {
|
|
5251
|
+
if (packet.deploy?.target !== LOCAL_SERVE_DEPLOY_TARGET
|
|
5252
|
+
&& polishCheckpointGate?.code === "polish.hidden_eager_media.no_capturable_routes"
|
|
5253
|
+
&& !packet.deploy?.preview_url) {
|
|
5254
|
+
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)}\`.`;
|
|
5255
|
+
}
|
|
5207
5256
|
const target = packet.deploy?.target || "unknown";
|
|
5208
5257
|
const liveUrlPath = packet.deploy?.live_url_path || packet.campaign?.live_url_path || campaignRouteRoot(packet) || "/<slug>/";
|
|
5209
5258
|
if (target === LOCAL_SERVE_DEPLOY_TARGET) {
|
|
@@ -5225,6 +5274,9 @@ Once the server is up:
|
|
|
5225
5274
|
|
|
5226
5275
|
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
5276
|
}
|
|
5277
|
+
if (polishGate?.carried_forward?.policy === "hosted_template_preview" && packet.deploy?.preview_url) {
|
|
5278
|
+
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("record")} deploy --packet ${shellToken(packetPath)} --base-url ${shellToken(packet.deploy.preview_url)}\` to record the deploy, then \`${qaRunCommand(packetPath, packet.deploy.preview_url)}\` to test the preview. Production QA remains strict.`;
|
|
5279
|
+
}
|
|
5228
5280
|
return `Deploy the built campaign to ${target}.
|
|
5229
5281
|
|
|
5230
5282
|
Read first:
|
|
@@ -5236,10 +5288,9 @@ Read first:
|
|
|
5236
5288
|
Deploy is currently an out-of-band step: the page-kit build produces _site/ output; you (or your CI) ship it to ${target}. Use the deploy target's normal tooling (netlify deploy, wrangler pages deploy, vercel deploy, etc.).
|
|
5237
5289
|
|
|
5238
5290
|
After deploy succeeds:
|
|
5239
|
-
1.
|
|
5240
|
-
2.
|
|
5241
|
-
3.
|
|
5242
|
-
4. Run \`${cmd("next")} --packet ${packetPath}\` to advance to QA.
|
|
5291
|
+
1. Run \`${cmd("record")} deploy --packet ${packetPath} --base-url <https preview origin>${liveUrlPath}\`. It requests every built page under that URL (each must answer HTTP 200) and compares every script and stylesheet the built pages load from that origin with the built output by sha256 (HTML is not compared; hosts inject markup), then records the URL at deploy.preview_url and stages.deploy as completed with the URL in outputs and the build fingerprint it probed. It refuses, writing nothing, naming each page or asset that does not match.
|
|
5292
|
+
2. Verify the SDK initialises on the tested origin. Localhost on any port is globally available as a Campaigns App Development domain (analytics suppressed). Non-localhost preview/production hosts must be in the Campaigns App SDK origin allowlist before QA.
|
|
5293
|
+
3. Run \`${cmd("next")} --packet ${packetPath}\` to advance to QA.
|
|
5243
5294
|
|
|
5244
5295
|
If the deploy is blocked (non-localhost allowed-domain not yet added, CI permission missing, host-side outage), set stages.deploy.status to "blocked" with a clear reason in outputs so the orchestration loop surfaces it rather than skipping past.`;
|
|
5245
5296
|
}
|
|
@@ -5247,36 +5298,40 @@ If the deploy is blocked (non-localhost allowed-domain not yet added, CI permiss
|
|
|
5247
5298
|
// The QA command the QA stage hands over. Given a bump cart it is the
|
|
5248
5299
|
// order-bump run: the same command with --cart selecting the base tier and
|
|
5249
5300
|
// 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
5301
|
// `qa run --test-order common` never puts a checkout order bump in a test
|
|
5255
5302
|
// order: the tier planner skips bump rows by design and bump coverage comes
|
|
5256
5303
|
// 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
|
-
//
|
|
5304
|
+
// the bump in the cart. It reads the one checkout path QA drives, with QA's own
|
|
5305
|
+
// row classifiers: QA's test orders run on the Checkout (the page that takes
|
|
5306
|
+
// payment) of the first entry path of the first funnel that has one, each
|
|
5307
|
+
// funnel's pages filtered to enabled and sorted by `order || 0`
|
|
5308
|
+
// (extractTopologies, findPaymentPage). The rows are read from every
|
|
5309
|
+
// checkout-flow page on that path — the select page or first step of a
|
|
5310
|
+
// multi-step checkout declares the cart, not the Checkout (campaigns-os#641).
|
|
5311
|
+
// When that path declares no bump it returns null rather than looking at a
|
|
5312
|
+
// later funnel: QA never drives a later funnel's checkout, so that funnel's
|
|
5313
|
+
// bump ref in --cart would land on a checkout without it. The base is the
|
|
5314
|
+
// first selector tier QA would plan: QA's default keeps the page's
|
|
5315
|
+
// pre-selected card, which a spec does not name. A path that declares no tier
|
|
5316
|
+
// (the cart is filled on an entry page) gets the bump alone, on whatever
|
|
5317
|
+
// selection the entry page made.
|
|
5267
5318
|
export function checkoutOrderBumpCart(spec) {
|
|
5268
|
-
const
|
|
5269
|
-
|
|
5270
|
-
|
|
5319
|
+
const upgraded = upgradeCampaignSpec(spec);
|
|
5320
|
+
const funnels = Array.isArray(upgraded?.funnels)
|
|
5321
|
+
? upgraded.funnels
|
|
5322
|
+
: Array.isArray(upgraded?.funnel_pages) ? [{ pages: upgraded.funnel_pages }] : [];
|
|
5271
5323
|
for (const funnel of funnels) {
|
|
5272
|
-
const
|
|
5324
|
+
const pages = (Array.isArray(funnel?.pages) ? funnel.pages : [])
|
|
5273
5325
|
.filter((page) => page && page.enabled !== false)
|
|
5274
|
-
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
|
5275
|
-
|
|
5326
|
+
.sort((a, b) => (a.order || 0) - (b.order || 0));
|
|
5327
|
+
const sorted = { ...funnel, pages };
|
|
5328
|
+
const checkout = paymentPageForFunnel(sorted);
|
|
5276
5329
|
if (!checkout) continue;
|
|
5277
|
-
const
|
|
5330
|
+
const entryPath = entryPages(sorted).map((entry) => checkoutPathFrom(pages, entry)).find((path) => path?.includes(checkout));
|
|
5331
|
+
const pathPages = (entryPath || [checkout]).filter((page) => isCheckoutFlowPage(page));
|
|
5332
|
+
const bumps = [...new Set(pathPages.flatMap((page) => declaredOrderBumps(page)))];
|
|
5278
5333
|
if (!bumps.length) return null;
|
|
5279
|
-
const base = declaredSelectorTiers(
|
|
5334
|
+
const base = pathPages.map((page) => declaredSelectorTiers(page)[0]?.ref).find(Boolean) || null;
|
|
5280
5335
|
return { base, bumps, cart: [base, ...bumps].filter(Boolean).map((ref) => `${ref}:1`).join(",") };
|
|
5281
5336
|
}
|
|
5282
5337
|
return null;
|
|
@@ -5288,7 +5343,12 @@ function orderBumpRunReason(bumpCart) {
|
|
|
5288
5343
|
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
5344
|
}
|
|
5290
5345
|
|
|
5291
|
-
function qaPrompt(packetPath, reportPath, packet, spec = null, intent) {
|
|
5346
|
+
function qaPrompt(packetPath, reportPath, packet, spec = null, intent, polishCheckpointGate = null) {
|
|
5347
|
+
if (packet.deploy?.target !== LOCAL_SERVE_DEPLOY_TARGET
|
|
5348
|
+
&& polishCheckpointGate?.code === "polish.hidden_eager_media.no_capturable_routes"
|
|
5349
|
+
&& !packet.deploy?.preview_url) {
|
|
5350
|
+
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)}\`.`;
|
|
5351
|
+
}
|
|
5292
5352
|
const url = packet.deploy?.preview_url || packet.deploy?.production_url || "<preview-url>";
|
|
5293
5353
|
const briefPath = packet.build_brief?.normalized_path || "(missing)";
|
|
5294
5354
|
const bumpCart = checkoutOrderBumpCart(spec);
|
|
@@ -5356,7 +5416,7 @@ function qaVerdictArtifactsForCampaign(packet, targetRepo) {
|
|
|
5356
5416
|
// Presence-based only: a stale built output still diverges from a pending
|
|
5357
5417
|
// ledger, and the entry says the two disagree — it never asserts the stage
|
|
5358
5418
|
// is complete or the artifact current. This function writes nothing.
|
|
5359
|
-
export function detectLedgerDivergence(report, packet, targetRepo) {
|
|
5419
|
+
export function detectLedgerDivergence(report, packet, targetRepo, { allowUnrecordedHostedPreview = false } = {}) {
|
|
5360
5420
|
const divergences = [];
|
|
5361
5421
|
const stages = report?.stages;
|
|
5362
5422
|
if (!isObject(stages) || !isNonEmptyString(targetRepo)) return divergences;
|
|
@@ -5389,7 +5449,7 @@ export function detectLedgerDivergence(report, packet, targetRepo) {
|
|
|
5389
5449
|
const packetPreviewUrl = optionalString(packet?.deploy?.preview_url);
|
|
5390
5450
|
const packetProductionUrl = optionalString(packet?.deploy?.production_url);
|
|
5391
5451
|
const url = reportUrl || packetPreviewUrl || packetProductionUrl;
|
|
5392
|
-
if (url) {
|
|
5452
|
+
if (url && !(allowUnrecordedHostedPreview && !reportUrl)) {
|
|
5393
5453
|
const source = reportUrl
|
|
5394
5454
|
? "report.stages.deploy.outputs"
|
|
5395
5455
|
: 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
|
}));
|