@nextcommerce/campaigns-os 1.54.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 +148 -0
- package/CONTEXT.md +19 -5
- package/README.md +26 -12
- 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 +1146 -0
- package/contracts/runtime-recipe.campaigns-os-node-v1.json +6 -1
- package/contracts/supported-surface.json +9 -5
- package/docs/brand-theme-bridge.md +10 -0
- package/docs/build-packet.md +86 -26
- package/docs/campaigns-os-build-flow.md +1 -1
- package/docs/effects.md +2 -5
- package/docs/local-setup.md +1 -1
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/qa-and-test-orders.md +133 -29
- 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/docs/workflow-findings-sidecar.md +57 -2
- package/package.json +4 -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/schemas/campaigns-os-run-record.v0.schema.json +20 -15
- 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 +5 -4
- package/skills.json +10 -10
- package/src/adapter-decision-contract.mjs +27 -13
- package/src/brand-theme.mjs +44 -8
- package/src/build-brief.mjs +14 -5
- package/src/campaign-identity.mjs +33 -4
- package/src/cli-helpers.mjs +129 -0
- package/src/cli.mjs +156 -111
- package/src/commercial-journey.mjs +25 -2
- package/src/consent.mjs +3 -2
- package/src/design-source-package.mjs +1 -1
- package/src/design-source-publication.mjs +8 -8
- package/src/deviation.mjs +2 -1
- package/src/doctor/checks.mjs +212 -19
- package/src/doctor/inspect.mjs +4 -2
- package/src/doctor/next-step.mjs +46 -16
- package/src/doctor-sidecar.mjs +5 -3
- package/src/gate-actions.mjs +21 -1
- package/src/input-refresh.mjs +6 -6
- package/src/invocation.mjs +4 -3
- package/src/lifecycle.mjs +88 -6
- package/src/local-preview-policy.mjs +116 -12
- package/src/login.mjs +2 -1
- package/src/polish-browser.mjs +8 -2
- package/src/polish-node.mjs +9 -4
- package/src/polish-readability.mjs +2 -2
- package/src/qa-browser.mjs +741 -120
- package/src/qa-cart-entry.mjs +107 -20
- package/src/qa-node.mjs +179 -72
- package/src/qa-test-order-topology.mjs +118 -1
- package/src/qa-verdict-discovery.mjs +21 -4
- package/src/qa-verdict-publish.mjs +2 -1
- package/src/run-record.mjs +46 -34
- package/src/runtime-state-ignore.mjs +7 -5
- package/src/sdk-markup.mjs +41 -1
- package/src/source-html-intake.mjs +46 -4
- package/src/stage-ledger.mjs +18 -4
- package/src/stage-record.mjs +53 -16
package/src/qa-cart-entry.mjs
CHANGED
|
@@ -70,12 +70,21 @@ export const CART_ENTRY_ROUTE_ATTRIBUTE = "data-next-url";
|
|
|
70
70
|
// preventDefault() unconditionally, so its href never navigates, and it
|
|
71
71
|
// resolves the attribute against the origin (campaign-cart url-utils), not the
|
|
72
72
|
// document base. Without the attribute such a control adds to the cart and
|
|
73
|
-
// stays put — no route.
|
|
73
|
+
// stays put — no route. A control matching one of `declaredRoutes` (each
|
|
74
|
+
// `{ selector, url }`, a route the page declares rather than one the control
|
|
75
|
+
// carries: the SDK checkout form's submit button and an upsell's accept /
|
|
76
|
+
// decline actions) goes to that url and nothing else, because the SDK
|
|
77
|
+
// handles the submission or the click itself, so an href="#" or the form's
|
|
78
|
+
// action never navigates. Anywhere else the SDK attribute has no navigation
|
|
74
79
|
// semantics: an HTML anchor's own resolved href (native, so a <base href> is
|
|
75
80
|
// honoured), then an href attribute, then `data-href`, then a wrapping form's
|
|
76
|
-
// action
|
|
77
|
-
// the SDK
|
|
78
|
-
|
|
81
|
+
// action — for that form's submit button only, and never inside a form
|
|
82
|
+
// matching `checkoutFormSelector`: the SDK checkout form, which the SDK
|
|
83
|
+
// submits itself, or, on a checkout or upsell / downsell page, every form,
|
|
84
|
+
// since there the only form-borne route is the one in `declaredRoutes`. Only
|
|
85
|
+
// spellings with real navigation semantics count — an attribute the SDK does
|
|
86
|
+
// not declare is not a route.
|
|
87
|
+
export function cartEntryHrefFor(element, { cartEntrySelector, cartEntryRouteAttribute, declaredRoutes = [], checkoutFormSelector = null, origin, baseHref }) {
|
|
79
88
|
if (!element || typeof element.getAttribute !== "function") return null;
|
|
80
89
|
if (typeof element.matches === "function" && element.matches(cartEntrySelector)) {
|
|
81
90
|
const sdkRoute = String(element.getAttribute(cartEntryRouteAttribute) || "").trim();
|
|
@@ -88,12 +97,26 @@ export function cartEntryHrefFor(element, { cartEntrySelector, cartEntryRouteAtt
|
|
|
88
97
|
return null;
|
|
89
98
|
}
|
|
90
99
|
}
|
|
100
|
+
const declared = typeof element.matches === "function"
|
|
101
|
+
? (declaredRoutes || []).find((route) => route?.selector && element.matches(route.selector))
|
|
102
|
+
: null;
|
|
103
|
+
if (declared) {
|
|
104
|
+
try {
|
|
105
|
+
return new URL(declared.url, baseHref).href;
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
91
110
|
// An HTML anchor resolves its own href (an SVG <a> exposes an object, not a
|
|
92
111
|
// string, and falls through to the attribute reading).
|
|
93
|
-
|
|
112
|
+
const tag = String(element.tagName || "").toUpperCase();
|
|
113
|
+
if (tag === "A" && typeof element.href === "string" && element.href) return element.href;
|
|
114
|
+
// The reflected type, so a <button> with no type attribute submits too.
|
|
115
|
+
const submits = (tag === "BUTTON" || tag === "INPUT") && String(element.type || "").toLowerCase() === "submit";
|
|
116
|
+
const insideRoutelessForm = Boolean(checkoutFormSelector) && typeof element.closest === "function" && Boolean(element.closest(checkoutFormSelector));
|
|
94
117
|
const attr = element.getAttribute("href")
|
|
95
118
|
|| element.getAttribute("data-href")
|
|
96
|
-
|| (typeof element.closest === "function" ? element.closest("form")?.getAttribute("action") : null);
|
|
119
|
+
|| (submits && !insideRoutelessForm && typeof element.closest === "function" ? element.closest("form")?.getAttribute("action") : null);
|
|
97
120
|
if (!attr) return null;
|
|
98
121
|
try {
|
|
99
122
|
return new URL(attr, baseHref).href;
|
|
@@ -104,6 +127,17 @@ export function cartEntryHrefFor(element, { cartEntrySelector, cartEntryRouteAtt
|
|
|
104
127
|
}
|
|
105
128
|
}
|
|
106
129
|
|
|
130
|
+
// A select page (campaigns-os#641) has no add-to-cart control: its swap-mode
|
|
131
|
+
// bundle selector writes the cart when a card is chosen, the SDK persists the
|
|
132
|
+
// cart between pages, and a checkout button or link moves on. The runner
|
|
133
|
+
// treats "bundle cards plus one of these" as a cart-entry control: the SDK's
|
|
134
|
+
// checkout action, or a plain link into the next page. Matched by this one
|
|
135
|
+
// selector so the chosen control replays with nth().
|
|
136
|
+
export const CART_ENTRY_CHECKOUT_BUTTON_SELECTOR = '[data-next-action="checkout"], a[href]';
|
|
137
|
+
|
|
138
|
+
// The bundle cards a select page renders, outside the cart summary.
|
|
139
|
+
export const CART_ENTRY_BUNDLE_CARD_SELECTOR = "[data-next-bundle-card]";
|
|
140
|
+
|
|
107
141
|
// Route-shaped spellings the runner used to consult and no longer does,
|
|
108
142
|
// because the SDK never declares them. They are not routes, but a page that
|
|
109
143
|
// carries one is reported so an operator can tell "the runner narrowed its
|
|
@@ -115,8 +149,10 @@ export const UNDECLARED_ROUTE_ATTRIBUTES = Object.freeze(["data-next-href", "dat
|
|
|
115
149
|
// shop-single-step landing declares; the rest are the entry-like types
|
|
116
150
|
// `deriveEntryUrls` already recognises.
|
|
117
151
|
// Pages that can only follow the checkout: the checkout itself plus the
|
|
118
|
-
// topology module's own offer and receipt sets, so the two never drift.
|
|
119
|
-
|
|
152
|
+
// topology module's own offer and receipt sets, so the two never drift. A
|
|
153
|
+
// checkout_step is never a cart entry either: it is a form page of the
|
|
154
|
+
// checkout path itself (campaigns-os#641).
|
|
155
|
+
const POST_CHECKOUT_PAGE_TYPES = new Set(["checkout", "checkout_step", ...OFFER_PAGE_TYPES, ...RECEIPT_PAGE_TYPES]);
|
|
120
156
|
const PREFERRED_ENTRY_PAGE_TYPES = ["select", "landing", "product", "presell", "entry", "lander", "advertorial", "listicle", "review", "opt-in", "optin"];
|
|
121
157
|
|
|
122
158
|
export function codedError(code, message) {
|
|
@@ -211,18 +247,22 @@ export function summarizeSelectionSurface(surface) {
|
|
|
211
247
|
return parts.length ? parts.join(", ") : "none";
|
|
212
248
|
}
|
|
213
249
|
|
|
214
|
-
// evaluate() body: the cart-entry controls the entry page renders.
|
|
215
|
-
// add_to_cart
|
|
216
|
-
//
|
|
217
|
-
// checkout_link
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
250
|
+
// evaluate() body: the cart-entry controls the entry page renders. Three kinds:
|
|
251
|
+
// add_to_cart the SDK action controls (CART_ENTRY_CONTROL_SELECTOR); the
|
|
252
|
+
// SDK adds the package and navigates via data-next-url;
|
|
253
|
+
// checkout_link a plain link into the checkout URL carrying
|
|
254
|
+
// `?forcePackageId=`, which the SDK reads on the checkout
|
|
255
|
+
// page to pre-load the cart. This is what the certified
|
|
256
|
+
// shop-single-step landing renders today;
|
|
257
|
+
// checkout_button on a page with bundle cards only: the SDK checkout action
|
|
258
|
+
// or a plain link into the checkout URL. The selected card
|
|
259
|
+
// is the cart; `bundle_package_ids` lists what the cards
|
|
260
|
+
// carry so --select-package can name one (campaigns-os#641).
|
|
221
261
|
// Each control reports its visibility, text, and the package id it carries
|
|
222
262
|
// (own attribute, nearest card, or the forcePackageId ref), so the caller can
|
|
223
263
|
// prefer a visible SDK control and honour --select-package in one round trip.
|
|
224
264
|
export function cartEntryControlsScript() {
|
|
225
|
-
return ({ selector, checkoutUrl }) => {
|
|
265
|
+
return ({ selector, checkoutUrl, checkoutButtonSelector = null, bundleCardSelector = null }) => {
|
|
226
266
|
const clean = (value) => String(value || "").replace(/\s+/g, " ").trim();
|
|
227
267
|
const isVisible = (element) => {
|
|
228
268
|
const rect = element.getBoundingClientRect();
|
|
@@ -290,7 +330,39 @@ export function cartEntryControlsScript() {
|
|
|
290
330
|
}];
|
|
291
331
|
})
|
|
292
332
|
: [];
|
|
293
|
-
|
|
333
|
+
const cards = Array.from(document.querySelectorAll(bundleCardSelector || "[data-next-bundle-card]"))
|
|
334
|
+
.filter((card) => !card.closest("[data-next-cart-summary], [data-next-upsell-context], [data-next-upsell], template"));
|
|
335
|
+
const bundlePackageIds = [...new Set(cards.flatMap((card) => {
|
|
336
|
+
const own = clean(card.getAttribute("data-next-package-id"));
|
|
337
|
+
const nested = Array.from(card.querySelectorAll("[data-next-package-id]")).map((node) => clean(node.getAttribute("data-next-package-id")));
|
|
338
|
+
return [own, ...nested].filter(Boolean);
|
|
339
|
+
}))];
|
|
340
|
+
const buttons = cards.length && checkoutButtonSelector
|
|
341
|
+
? Array.from(document.querySelectorAll(checkoutButtonSelector)).flatMap((element, index) => {
|
|
342
|
+
const isAction = element.getAttribute("data-next-action") === "checkout";
|
|
343
|
+
if (!isAction) {
|
|
344
|
+
const href = element.getAttribute("href");
|
|
345
|
+
if (!checkout || canonical(href) !== checkout) return [];
|
|
346
|
+
try {
|
|
347
|
+
if (new URL(href, document.baseURI).searchParams.get("forcePackageId")) return [];
|
|
348
|
+
} catch {
|
|
349
|
+
return [];
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return [{
|
|
353
|
+
kind: "checkout_button",
|
|
354
|
+
index,
|
|
355
|
+
visible: isVisible(element),
|
|
356
|
+
text: label(element),
|
|
357
|
+
package_id: null,
|
|
358
|
+
bundle_package_ids: bundlePackageIds,
|
|
359
|
+
quantity: 1,
|
|
360
|
+
next_url: isAction ? null : element.getAttribute("href"),
|
|
361
|
+
tag: element.tagName.toLowerCase(),
|
|
362
|
+
}];
|
|
363
|
+
})
|
|
364
|
+
: [];
|
|
365
|
+
return [...actions, ...links, ...buttons];
|
|
294
366
|
};
|
|
295
367
|
}
|
|
296
368
|
|
|
@@ -300,7 +372,7 @@ export function cartEntryControlsScript() {
|
|
|
300
372
|
// control is only used when nothing is visible.
|
|
301
373
|
export function chooseCartEntryControl(controls = [], requested = []) {
|
|
302
374
|
const list = Array.isArray(controls) ? controls : [];
|
|
303
|
-
if (!list.length) return { control: null, reason: "no add-to-cart control
|
|
375
|
+
if (!list.length) return { control: null, reason: "no add-to-cart control, forcePackageId checkout link, or bundle cards with a checkout button rendered on the entry page" };
|
|
304
376
|
const wanted = Array.isArray(requested) ? requested.filter((item) => item?.packageId) : [];
|
|
305
377
|
if (wanted.length > 1) {
|
|
306
378
|
return {
|
|
@@ -308,9 +380,24 @@ export function chooseCartEntryControl(controls = [], requested = []) {
|
|
|
308
380
|
reason: `--select-package requested ${wanted.length} refs, but a landing-page entry can select at most one package before the SDK navigates to checkout`,
|
|
309
381
|
};
|
|
310
382
|
}
|
|
383
|
+
// A select page's checkout button carries no package of its own: the
|
|
384
|
+
// requested package is the bundle card the runner clicks before it. Used
|
|
385
|
+
// only when no add-to-cart control or forcePackageId link carries the ref.
|
|
386
|
+
const buttons = list.filter((control) => control.kind === "checkout_button");
|
|
387
|
+
const carriers = list.filter((control) => control.kind !== "checkout_button");
|
|
311
388
|
const byRef = wanted.length
|
|
312
|
-
?
|
|
313
|
-
:
|
|
389
|
+
? carriers.filter((control) => String(control.package_id) === String(wanted[0].packageId))
|
|
390
|
+
: carriers;
|
|
391
|
+
if (!byRef.length && buttons.length) {
|
|
392
|
+
const pick = (pool) => pool.find((control) => control.visible) || pool[0];
|
|
393
|
+
if (!wanted.length) return { control: pick(buttons), reason: null };
|
|
394
|
+
const withCard = buttons.filter((control) => (control.bundle_package_ids || []).map(String).includes(String(wanted[0].packageId)));
|
|
395
|
+
if (withCard.length) return { control: pick(withCard), select_card: wanted[0], reason: null };
|
|
396
|
+
return {
|
|
397
|
+
control: null,
|
|
398
|
+
reason: `--select-package ${wanted[0].packageId}: no bundle card on the entry page carries package ${wanted[0].packageId} (rendered: ${[...new Set(buttons.flatMap((control) => control.bundle_package_ids || []))].join(", ") || "(none)"})`,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
314
401
|
if (!byRef.length) {
|
|
315
402
|
return {
|
|
316
403
|
control: null,
|